diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml new file mode 100644 index 0000000000000..7bd754e0a539a --- /dev/null +++ b/.github/workflows/api-tests.yml @@ -0,0 +1,36 @@ +name: API Tests + +on: + push: + paths: + - 'tests/api/**' + - '.github/workflows/api-tests.yml' + pull_request: + paths: + - 'tests/api/**' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest requests + + - name: Run tests + env: + API_TOKEN: ${{ secrets.GITEA_API_TOKEN }} + run: | + echo "BASE_URL = 'http://52.211.112.5:3000/'" > config.py + echo "API_TOKEN = '${API_TOKEN}'" >> config.py + pytest tests/api -v diff --git a/gitea-tests.pem b/gitea-tests.pem new file mode 100644 index 0000000000000..65c9ec0e01248 --- /dev/null +++ b/gitea-tests.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAtpFEkGJbDhqrrjAk44uAf5Wr5/nPTso1hGdXVyeXKY/f44kk +G03FUdcydLtO9SeMZeuHjDNOZIDf4N5eEMEYQWZazJRRESzFG1XzokqO4NqjqlxX +o/67hxM+UGiZAlgOyPwQj9ewhg5ZMrv2kcHbTmEyxiTkbkb+d9+ryqKkAlO3KqJW +P9SU3mtuCLG01Tqm0dBaZgxnpLqjBK/OOMW4G8KKl6q92+7GEF/XNP8G5gWDdMiI +fEMzRVorqALGxKTjcvMgIKG9dmeI5hE7cgnVzdlQ6xSZ2i/awGPNGF33VIaHCU3S +Upve8nxsybizEijkXuhbhnhco2w+E1iEBgmnRQIDAQABAoIBAFArjItG1atk5N0S +ATD28o+UPzAYEAQOYd/prX31Qbkbl/qIH5Xp2ettb5e5JRwcqNeczSIw6YzS1v0d +SPtcf/VAKEFMJClBmrC9VsZ+rS1qdZJ7CHVYiCrxtVyEAiT4XE2/+tnfooHLzTmt +NsKc+Vv67Nv8GV+fx2EGlJ7gOttV/8Hy3oT4jqkvTJWlenzxOJdGQiVXtos8YpX8 +MM/PkS5Sauu6OU5ArSXXHg7C/bNOYgWKXfDP9MQn8IxflSzVAOn+Ad7dXkjTC3Ua +a4xv9PeRVekfEi3WmahQxhXDVTrVZwiGUJ8hxZMvNteNPMWcSJdlo014Ly/Qjx5M +jlBKhOUCgYEA4UygCtTbSf5zeilqESqfnAzH+yhieAKA2E6g67zX0h76UKMiU1Vc +efNQEiFYFeIbhXUBLCdiT7C8gpgm5QvUWpsNFjqSKV844ZBc2TZXsB6J/JEZKUuN +6x3SRKNmuFtiW7J78hdLCHj/l+FukdtSRBNQco+cOFs5HT55qLD8LvcCgYEAz3H6 +WxM876n7npxJyCtXkyYuMQGafqUk4WHWtlPTWIQw7DwqgeF0p4SOkwYIabI6ILEY +gAAbQVrFmChvh/tQmDk29gubKdMe7JM3nIRXBPaDwSxnBs0EjgAJiXdYlpCCHVIX +4Za2Vi1M9N7cGZecVT0sBUuMaBmya/IBeyOWQKMCgYBX46pzT0IUhXzK5SkJdVU6 +bQn+gmyXYHKe7117WPngcFE578nONHiU4kQULonMT55o25IPhXWmnM2NLInPxGOc +zOu4BjVKimkIJWbzHW3ruJ4ftwLXxy+fzsxeFlhWBuBB4UjU0h1lOr6Ko1ic8bAP ++nDhoABTQ9LuA5c2JYTbVQKBgC7CDiBBMdcDhYe2ypqnylGMpZS+O8iYCLwUhYUL +V/P3t99HoH0uCFFJ+6kADx1j4t5DjLYtT/dnMmqdkqYf64akPtMuwoam462HcV2C +JusjdYcxLvfFdmVbdMrbb8hgQjPBsUhT5D2AcHwxT4MlPUOpSibXZIqCYEkcf2D2 +IIPpAoGADaSAsjcGFBWgQco2i/gUHxMuNE425mcNcXQ+zutSNyZ2TVpLXJukIjsx +cB8YqTQ16X0wls79fu92A4QfYpG4POXn+usD9T5r4R/b+ofZCTHEYi1j4MCcUhcQ +AK6vnjryobdhniZCFREdcX0yPjT0ZC0pZcVarqVu4tLoRHFLqcs= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/api/__pycache__/test_users.cpython-312-pytest-8.4.1.pyc b/tests/api/__pycache__/test_users.cpython-312-pytest-8.4.1.pyc new file mode 100644 index 0000000000000..03bec7c8b04eb Binary files /dev/null and b/tests/api/__pycache__/test_users.cpython-312-pytest-8.4.1.pyc differ diff --git a/tests/api/config.py b/tests/api/config.py new file mode 100644 index 0000000000000..06eb2fa967f88 --- /dev/null +++ b/tests/api/config.py @@ -0,0 +1,10 @@ +import os + +#local API_TOKEN +#API_TOKEN = 'b32712d989aeee1e78f112ba4fc8e9f93ae7fe78' + +#remote API_TOKEN: +API_TOKEN = 'f1600abdf735b9e11dbfc45e57db04319d58235e' + +#BASE_URL = os.getenv('GITEA_URL', 'http://localhost:3000') +BASE_URL = 'http://52.211.112.5:3000' \ No newline at end of file diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py new file mode 100644 index 0000000000000..fca27a390b0bd --- /dev/null +++ b/tests/api/test_auth.py @@ -0,0 +1,16 @@ +import requests +from config import BASE_URL, API_TOKEN # uses the same config.py you already generate in CI + +def test_me_endpoint_returns_current_user_and_is_admin(): + headers = {"Authorization": f"token {API_TOKEN}"} + + r = requests.get(f"{BASE_URL}/api/v1/user", headers=headers) + assert r.status_code == 200, f"Unexpected status: {r.status_code} body={r.text}" + + me = r.json() + # Basic shape checks + for key in ["id", "login", "email", "is_admin", "created"]: + assert key in me, f"Missing key '{key}' in /user response: {me}" + + # If the token is an admin (your token is), this should be True + assert me["is_admin"] is True diff --git a/tests/api/test_branches.py b/tests/api/test_branches.py new file mode 100644 index 0000000000000..ef6fe4ee70c76 --- /dev/null +++ b/tests/api/test_branches.py @@ -0,0 +1,50 @@ +import requests +import random +import string +from config import BASE_URL, API_TOKEN + +def _headers(): + return {"Authorization": f"token {API_TOKEN}"} + +def _rand(prefix): + return prefix + "_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + +def test_list_branches_contains_default_branch(): + headers = _headers() + + # 1) Create owner user + username = _rand("owner") + user_payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False, + } + r_user = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=user_payload) + assert r_user.status_code == 201, f"User create failed: {r_user.status_code} {r_user.text}" + + # 2) Create repo with auto_init so default branch exists + repo_name = _rand("repo") + repo_payload = {"name": repo_name, "description": "Branches test", "private": False, "auto_init": True} + r_repo = requests.post(f"{BASE_URL}/api/v1/admin/users/{username}/repos", headers=headers, json=repo_payload) + assert r_repo.status_code == 201, f"Repo create failed: {r_repo.status_code} {r_repo.text}" + + # 3) Get repo to read its default_branch + r_get = requests.get(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}", headers=headers) + assert r_get.status_code == 200, f"Get repo failed: {r_get.status_code} {r_get.text}" + default_branch = r_get.json().get("default_branch") + assert default_branch, "Expected default_branch to be set" + + # 4) List branches and verify default branch is present + r_branches = requests.get(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}/branches", headers=headers) + assert r_branches.status_code == 200, f"List branches failed: {r_branches.status_code} {r_branches.text}" + branches = r_branches.json() + assert isinstance(branches, list) and branches, "Expected non-empty branches list" + + names = {b.get("name") for b in branches} + assert default_branch in names, f"default branch '{default_branch}' not found in {names}" + + # 5) Cleanup (best-effort) + requests.delete(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}", headers=headers) + requests.delete(f"{BASE_URL}/api/v1/admin/users/{username}", headers=headers) diff --git a/tests/api/test_duplicate_users.py b/tests/api/test_duplicate_users.py new file mode 100644 index 0000000000000..db8d1c1c0665f --- /dev/null +++ b/tests/api/test_duplicate_users.py @@ -0,0 +1,38 @@ +import requests +import string +import random +from config import BASE_URL, API_TOKEN + +def _headers(): + return {"Authorization": f"token {API_TOKEN}"} + +def _rand_username(prefix="dupuser"): + return prefix + "_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + +def test_create_duplicate_user_should_fail(): + headers = _headers() + username = _rand_username() + payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False, + } + + # First creation should succeed + r1 = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=payload) + assert r1.status_code == 201, f"First create failed: {r1.status_code} {r1.text}" + + # Second creation with the same username should fail + r2 = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=payload) + + # Gitea typically returns 422 for duplicate username; some setups may return 409 + assert r2.status_code in (422, 409), f"Expected 422/409 on duplicate, got {r2.status_code} {r2.text}" + + # Error body should hint user exists + err_text = r2.text.lower() + assert ("exist" in err_text) or ("already" in err_text), f"Unexpected error message: {r2.text}" + + # Cleanup: best-effort delete the user we created (ignore errors) + requests.delete(f"{BASE_URL}/api/v1/admin/users/{username}", headers=headers) diff --git a/tests/api/test_issues.py b/tests/api/test_issues.py new file mode 100644 index 0000000000000..62013542791d5 --- /dev/null +++ b/tests/api/test_issues.py @@ -0,0 +1,58 @@ +import requests +import random +import string +from config import BASE_URL, API_TOKEN + +def _headers(): + return {"Authorization": f"token {API_TOKEN}"} + +def _rand(prefix): + return prefix + "_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + +def test_create_issue_and_list_it(): + headers = _headers() + + # 1) Create owner user + username = _rand("owner") + user_payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False, + } + r_user = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=user_payload) + assert r_user.status_code == 201, f"User create failed: {r_user.status_code} {r_user.text}" + + # 2) Create repo + repo_name = _rand("repo") + repo_payload = {"name": repo_name, "description": "Issues test", "private": False, "auto_init": True} + r_repo = requests.post(f"{BASE_URL}/api/v1/admin/users/{username}/repos", headers=headers, json=repo_payload) + assert r_repo.status_code == 201, f"Repo create failed: {r_repo.status_code} {r_repo.text}" + + # 3) Create an issue + title = f"Issue { _rand('t') }" + body = "Created by API test" + issue_payload = {"title": title, "body": body} + r_issue = requests.post( + f"{BASE_URL}/api/v1/repos/{username}/{repo_name}/issues", + headers=headers, + json=issue_payload + ) + assert r_issue.status_code == 201, f"Issue create failed: {r_issue.status_code} {r_issue.text}" + issue = r_issue.json() + number = issue.get("number") + assert number, f"Expected issue number, got {issue}" + assert issue["title"] == title + assert issue["state"] == "open" + + # 4) List issues and verify the created one is present + r_list = requests.get(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}/issues", headers=headers) + assert r_list.status_code == 200, f"Issues list failed: {r_list.status_code} {r_list.text}" + issues = r_list.json() + numbers = {i.get("number") for i in issues} + assert number in numbers, f"Issue #{number} not found in list: {numbers}" + + # 5) Cleanup (best-effort) + requests.delete(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}", headers=headers) + requests.delete(f"{BASE_URL}/api/v1/admin/users/{username}", headers=headers) diff --git a/tests/api/test_repos.py b/tests/api/test_repos.py new file mode 100644 index 0000000000000..4c64be4b21f6c --- /dev/null +++ b/tests/api/test_repos.py @@ -0,0 +1,55 @@ +import requests +import random +import string +from config import BASE_URL, API_TOKEN + +def _headers(): + return {"Authorization": f"token {API_TOKEN}"} + +def _rand(s="user"): + return s + "_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + +def test_create_repo_then_get_details(): + headers = _headers() + + # 1) Create a user (owner) + username = _rand("owner") + user_payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False, + } + r_user = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=user_payload) + assert r_user.status_code == 201, f"User create failed: {r_user.status_code} {r_user.text}" + + # 2) Create a repository for that user + repo_name = _rand("repo") + repo_payload = { + "name": repo_name, + "description": "API test repo", + "private": False, + "auto_init": True, # so default branch & README exist + } + r_repo = requests.post(f"{BASE_URL}/api/v1/admin/users/{username}/repos", headers=headers, json=repo_payload) + assert r_repo.status_code == 201, f"Repo create failed: {r_repo.status_code} {r_repo.text}" + created = r_repo.json() + assert created["name"] == repo_name + assert created["owner"]["login"] == username + + # 3) GET repo details and verify fields + r_get = requests.get(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}", headers=headers) + assert r_get.status_code == 200, f"Get repo failed: {r_get.status_code} {r_get.text}" + repo = r_get.json() + + # Basic shape checks + assert repo["name"] == repo_name + assert repo["owner"]["login"] == username + assert repo["private"] is False + # default branch commonly 'main' or 'master' depending on settings; assert it's non-empty + assert repo.get("default_branch"), f"Expected default_branch to be set, got {repo.get('default_branch')}" + + # 4) Cleanup (best-effort) + requests.delete(f"{BASE_URL}/api/v1/repos/{username}/{repo_name}", headers=headers) + requests.delete(f"{BASE_URL}/api/v1/admin/users/{username}", headers=headers) diff --git a/tests/api/test_users.py b/tests/api/test_users.py new file mode 100644 index 0000000000000..c4221dcc2e6f4 --- /dev/null +++ b/tests/api/test_users.py @@ -0,0 +1,103 @@ +import requests +import random +import string + +# Load from config.py written in GitHub Actions +from config import BASE_URL, API_TOKEN + +def test_get_users(): + headers = { + "Authorization": f"token {API_TOKEN}" + } + + response = requests.get(f"{BASE_URL}/api/v1/admin/users", headers=headers) + + assert response.status_code == 200 + users = response.json() + assert isinstance(users, list) + + +def generate_random_username(prefix="user"): + suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5)) + return f"{prefix}_{suffix}" + +def test_create_user(): + headers = { + "Authorization": f"token {API_TOKEN}" + } + + username = generate_random_username() + payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False + } + + response = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=payload) + + assert response.status_code == 201, f"Response: {response.text}" + user = response.json() + assert user["username"] == username + +def test_list_user_repos(): + headers = { + "Authorization": f"token {API_TOKEN}" + } + + # Create a new user first + username = generate_random_username() + payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False + } + create_response = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=payload) + assert create_response.status_code == 201 + + # Now fetch their repositories + list_response = requests.get(f"{BASE_URL}/api/v1/users/{username}/repos", headers=headers) + + assert list_response.status_code == 200 + repos = list_response.json() + assert isinstance(repos, list) + assert len(repos) == 0 # New user should have no repos + +def test_create_repo_for_user(): + headers = { + "Authorization": f"token {API_TOKEN}" + } + + # Step 1: Create a new user + username = generate_random_username() + payload = { + "email": f"{username}@example.com", + "username": username, + "password": "TestPass123!", + "must_change_password": False, + "send_notify": False + } + create_user_response = requests.post(f"{BASE_URL}/api/v1/admin/users", headers=headers, json=payload) + assert create_user_response.status_code == 201 + + # Step 2: Create a repository for the new user + repo_name = f"repo_{username}" + repo_payload = { + "name": repo_name, + "description": "This is a test repo", + "private": False, + "auto_init": True + } + create_repo_response = requests.post( + f"{BASE_URL}/api/v1/admin/users/{username}/repos", + headers=headers, + json=repo_payload + ) + + assert create_repo_response.status_code == 201, f"Error: {create_repo_response.text}" + repo = create_repo_response.json() + assert repo["name"] == repo_name + assert repo["owner"]["login"] == username