Some checks failed
continuous-integration/drone/push Build is failing
- tests/gitea/recipe_meta.py: updated from dep-provider stub to dual-role (dep + recipe-under-test).
Adds BACKUP_CAPABLE=True, READY_PROBE (/api/v1/version), SCREENSHOT (sign-in page), LFS-
conditional EXTRA_ENV (compose.lfs.yml + GITEA_LFS_START_SERVER only when RECIPE=gitea AND
overlay present — dep path unchanged). All existing dep keys preserved; 10/10 dep unit tests pass.
- tests/gitea/ops.py: NEW — admin user creation via gitea CLI (ci_admin, creds in /tmp per-domain
file), marker repo lifecycle (pre_install/pre_upgrade/pre_backup create; pre_restore deletes to
diverge from backup state).
- tests/gitea/test_{install,upgrade,backup,restore}.py: NEW — lifecycle overlays. Install checks
API + admin auth + Playwright sign-in. Upgrade/backup/restore assert marker repo continuity.
- tests/gitea/custom/: NEW — test_health.py (parity: HTTP 200 root), test_git_push.py (parity:
create→clone→push→verify→delete), test_admin_api.py (beyond-parity: user+org+token CRUD),
test_lfs_roundtrip.py (LFS OID round-trip + JWT stability; skips on main, runs on PR #1 head).
- tests/gitea/PARITY.md: NEW — mapping table, source note (recipe-info corpus not upstream repo),
beyond-parity rationale, backup/restore real-tier note, DB choice, dep-split mechanism, LFS skip.
- machine-docs/STATUS-gtea.md: NEW — phase status (building M1).
- machine-docs/BACKLOG-gtea.md: merged with Adversary init.
- machine-docs/JOURNAL-gtea.md: Builder log with design decisions + unit test results.
- machine-docs/REVIEW-gtea.md: kept Adversary init content.
- machine-docs/DECISIONS.md: appended gtea section (LFS split, admin mgmt, marker design).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""gitea — beyond-parity: admin API CRUD lifecycle (phase gtea).
|
|
|
|
Proves the gitea admin REST API: user + org + token creation, read-back, and deletion.
|
|
Non-vacuous: a misconfigured gitea (broken DB, broken API routing) fails any of these.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import secrets
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
from ops import admin_creds # noqa: E402
|
|
|
|
import ssl
|
|
|
|
_CTX = ssl.create_default_context()
|
|
_CTX.check_hostname = False
|
|
_CTX.verify_mode = ssl.CERT_NONE
|
|
|
|
|
|
def _api(domain, path, method="GET", body=None, user="", password="", token=""):
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
if token:
|
|
headers = {"Authorization": f"token {token}"}
|
|
else:
|
|
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
headers = {"Authorization": f"Basic {auth}"}
|
|
if data:
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(
|
|
f"https://{domain}/api/v1{path}", data=data, headers=headers, method=method
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20, context=_CTX) as r:
|
|
raw = r.read()
|
|
return r.status, (json.loads(raw) if raw else {})
|
|
except urllib.error.HTTPError as e:
|
|
raw = e.read()
|
|
try:
|
|
return e.code, json.loads(raw)
|
|
except (ValueError, json.JSONDecodeError):
|
|
return e.code, {}
|
|
|
|
|
|
def test_admin_api_user_org_token_lifecycle(live_app):
|
|
"""Admin API CRUD: create user → create org → create API token → read-back → delete.
|
|
|
|
Proves the full admin-API lifecycle:
|
|
1. Create a test user (admin-creates-user API)
|
|
2. Create an org (admin-creates-org API)
|
|
3. Create an API token for the test user (user-generates-token API)
|
|
4. Read back the user + org via the token
|
|
5. Delete the token, org, and user (cleanup)
|
|
"""
|
|
adm_user, adm_pass = admin_creds(live_app)
|
|
suffix = secrets.token_hex(4)
|
|
test_user = f"testuser-{suffix}"
|
|
test_org = f"testorg-{suffix}"
|
|
token_name = f"ci-test-token-{suffix}"
|
|
test_pass = secrets.token_hex(12) + "A1" # at least one uppercase + digit for any policy
|
|
|
|
# 1. Create test user
|
|
status, body = _api(
|
|
live_app, "/admin/users", method="POST",
|
|
body={
|
|
"username": test_user,
|
|
"email": f"{test_user}@ci.local",
|
|
"password": test_pass,
|
|
"must_change_password": False,
|
|
"login_name": test_user,
|
|
"source_id": 0,
|
|
},
|
|
user=adm_user, password=adm_pass,
|
|
)
|
|
assert status == 201, f"user create HTTP {status}: {body}"
|
|
assert body.get("login") == test_user, f"unexpected login: {body}"
|
|
|
|
try:
|
|
# 2. Create org (as admin, add test user as member)
|
|
status, body = _api(
|
|
live_app, "/orgs", method="POST",
|
|
body={"username": test_org, "visibility": "public"},
|
|
user=adm_user, password=adm_pass,
|
|
)
|
|
assert status == 201, f"org create HTTP {status}: {body}"
|
|
assert body.get("username") == test_org, f"unexpected org: {body}"
|
|
|
|
try:
|
|
# 3. Create API token for test user (admin creates token on behalf of user)
|
|
status, tok_body = _api(
|
|
live_app, f"/users/{test_user}/tokens", method="POST",
|
|
body={"name": token_name},
|
|
user=adm_user, password=adm_pass,
|
|
)
|
|
assert status == 201, f"token create HTTP {status}: {tok_body}"
|
|
token = tok_body.get("sha1")
|
|
assert token, f"token sha1 missing from response: {tok_body}"
|
|
|
|
# 4. Read back via token: authenticated call as test_user
|
|
status, me = _api(live_app, "/user", token=token)
|
|
assert status == 200, f"GET /user with token HTTP {status}"
|
|
assert me.get("login") == test_user, f"token authenticated as wrong user: {me}"
|
|
|
|
# 5. Read org via token
|
|
status, org = _api(live_app, f"/orgs/{test_org}", token=token)
|
|
assert status == 200, f"GET /orgs/{test_org} HTTP {status}: {org}"
|
|
assert org.get("username") == test_org
|
|
|
|
# 6. Delete the token
|
|
status, _ = _api(
|
|
live_app, f"/users/{test_user}/tokens/{token_name}", method="DELETE",
|
|
user=adm_user, password=adm_pass,
|
|
)
|
|
assert status in (204, 404), f"token delete HTTP {status}"
|
|
|
|
finally:
|
|
# Delete org
|
|
_api(live_app, f"/orgs/{test_org}", method="DELETE", user=adm_user, password=adm_pass)
|
|
|
|
finally:
|
|
# Delete test user (admin only)
|
|
_api(live_app, f"/admin/users/{test_user}", method="DELETE",
|
|
user=adm_user, password=adm_pass)
|