feat: declare API errors and authentication with developer tooling

This commit is contained in:
2026-09-23 18:17:43 +08:00
parent ab8eac5fc4
commit 2913e94a0a
35 changed files with 2246 additions and 107 deletions

View File

@@ -59,9 +59,9 @@ class LimitedMySQL(MySQL):
def exercise(binary, project, env, db, transport):
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0)); port = sock.getsockname()[1]
token, signing = secrets.token_hex(24), secrets.token_hex(24)
token, signing, readToken = secrets.token_hex(24), secrets.token_hex(24), secrets.token_hex(24)
environment = dict(env, GO_OMNI_HTTP_ADDR=f"127.0.0.1:{port}", GO_OMNI_TRANSPORT=transport,
USER_SERVICE_API_TOKEN=token, GO_OMNI_STORAGE_SIGNING_KEY=signing,
USER_SERVICE_API_TOKEN=token, USER_SERVICE_READ_TOKEN=readToken, GO_OMNI_STORAGE_SIGNING_KEY=signing,
GO_OMNI_STORAGE_ROOT=str(project / (transport + "Files")),
GO_OMNI_STORAGE_PUBLIC_URL=f"http://127.0.0.1:{port}/storage/object")
logs = project / "var/log/server.log"
@@ -72,7 +72,7 @@ def exercise(binary, project, env, db, transport):
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def request(method, path, body=None, expected=200, auth=True):
headers = {"Content-Type": "application/json", "X-Request-ID": "apiWorkflow"}
if auth: headers["Authorization"] = "Bearer " + token
if auth: headers["Authorization"] = "Bearer " + (token if auth is True else auth)
req = urllib.request.Request(f"http://127.0.0.1:{port}" + path,
data=None if body is None else json.dumps(body).encode(), headers=headers, method=method)
try: response = client.open(req, timeout=5)
@@ -98,6 +98,10 @@ def exercise(binary, project, env, db, transport):
process = start()
try:
request("GET", "/api/v1/users", expected=401, auth=False); checks.append("authBeforeService")
request("GET", "/api/v1/users", auth=readToken)
denied = request("POST", "/api/v1/users", {"name": "NoWrite", "email": "readonly@example.com"}, 403, auth=readToken)
assert denied["code"] == "FORBIDDEN"
checks.append("readOnlyPermission403")
empty = request("GET", "/api/v1/users")["data"]
assert empty["items"] == [] and empty["total"] == 0
checks.append("emptyPageAndQueryDefaults")
@@ -143,6 +147,11 @@ def exercise(binary, project, env, db, transport):
checks.append("signedDownloadTamperRejected")
doc = request("GET", "/openapi.json")
assert doc == json.loads((project / "docs/openapi.json").read_text()); checks.append("openAPIMatchesContract")
createDoc = doc["paths"]["/api/v1/users"]["post"]
assert createDoc["security"] == [{"AdminAuth": []}]
for status, name, code in (("401", "Unauthenticated", "UNAUTHENTICATED"), ("403", "Forbidden", "FORBIDDEN"), ("409", "EmailTaken", "EMAIL_TAKEN")):
assert createDoc["responses"][status]["content"]["application/json"]["examples"][name]["value"]["code"] == code
checks.append("declaredAuthAndBusinessErrorDocumentation")
run(["docker", "stop", "--time=3", db.container], project, db.env)
request("GET", "/health/ready", expected=503)
run(["docker", "start", db.container], project, db.env); db.wait()
@@ -162,7 +171,7 @@ def exercise(binary, project, env, db, transport):
text = logs.read_text()
entries = [json.loads(line) for line in text.splitlines() if line.startswith("{")]
assert any(e.get("msg") == "user.created" and e.get("requestId") == "apiWorkflow" for e in entries)
assert all(secret not in text for secret in (token, signing, env["GO_OMNI_DB_DSN"], link, "updated@example.com"))
assert all(secret not in text for secret in (token, signing, readToken, env["GO_OMNI_DB_DSN"], link, "updated@example.com"))
checks.append("businessFileLogsAndSecretExclusion")
return {"transport": transport, "checks": checks}
finally: