feat: complete API-driven projects with infrastructure and business acceptance

This commit is contained in:
2026-09-23 17:33:50 +08:00
parent 04a1f7999d
commit 2c74dc0859
137 changed files with 11926 additions and 105 deletions

233
scripts/verifyAPI.py Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Bounded local acceptance of API-generated projects, on both Omni transports.
No remote hosts, cloud credentials or database are used. Child servers and
temporary modules are always cleaned up. Run from any directory with Python 3.
"""
import hashlib
import http.client
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import tempfile
import time
ROOT = Path(__file__).resolve().parents[1]
REPORT = ROOT / "performance/apiGenerationLocal.json"
ENV = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
ENV.update(GOWORK="off", GOFLAGS="")
def run(args, cwd=ROOT, ok=True):
result = subprocess.run([str(x) for x in args], cwd=cwd, env=ENV,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=90)
if ok and result.returncode:
raise AssertionError(f"{args}:\n{result.stdout}")
if not ok and not result.returncode:
raise AssertionError(f"command unexpectedly succeeded: {args}")
return result.stdout
def request(port, method, path, body=None, headers=None):
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
try:
if body is not None and len(body) > (1 << 20):
# Probe early Content-Length rejection without racing a 1 MiB
# client send against the server's intentional connection close.
conn.putrequest(method, path)
for key, value in (headers or {}).items():
conn.putheader(key, value)
conn.putheader("Content-Length", str(len(body)))
conn.endheaders()
else:
conn.request(method, path, body=body, headers=headers or {})
response = conn.getresponse()
return response.status, dict(response.getheaders()), response.read()
finally:
conn.close()
def write(path, text):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
def exercise(binary, project, transport, starter):
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
environment = dict(ENV, GO_OMNI_HTTP_ADDR=f"127.0.0.1:{port}",
GO_OMNI_TRANSPORT=transport, GO_OMNI_LOG_OUTPUT="stderr",
GO_OMNI_LOG_FORMAT="json")
checks = []
log_path = project / f"{transport}.log"
with log_path.open("w+") as log:
process = subprocess.Popen([str(binary)], cwd=project, env=environment,
stdout=log, stderr=log)
try:
deadline = time.monotonic() + 10
while True:
if process.poll() is not None:
raise AssertionError(log_path.read_text())
try:
if request(port, "GET", "/health/ready")[0] == 200:
break
except OSError:
pass
if time.monotonic() > deadline:
raise AssertionError("server startup timed out")
time.sleep(0.05)
checks.append("readiness")
status, headers, document = request(port, "GET", "/openapi.json")
assert status == 200 and document == (project / "docs/openapi.json").read_bytes()
checks.append("servedOpenApiMatchesFile")
if starter:
status, headers, data = request(port, "GET", "/api/greet/Alice")
assert status == 200 and json.loads(data)["data"]["message"] == "Hello Alice"
assert headers.get("X-Request-Id") or headers.get("X-Request-ID")
checks.extend(["greeting", "requestId"])
else:
body = json.dumps({"name": "Alice", "active": False, "age": 0})
base = {"Content-Type": "application/json", "X-Request-ID": "api-acceptance-42"}
for method, path, payload, expected in [
("PUT", "/api/v1/users/7?notify=true", body, 200),
("PUT", "/api/v1/users/7", '{"name":"Alice"}', 400),
("PUT", "/api/v1/users/7?notify=bad", body, 400),
("PUT", "/api/v1/users/7?notify=true&notify=false", body, 400),
("PUT", "/api/v1/users/7", body[:-1] + ',"extra":1}', 400),
("PUT", "/api/v1/users/7", body + "{}", 400),
("PUT", "/api/v1/users/7", '{"name":"A","active":false,"age":151}', 400),
("PUT", "/api/v1/users/0", body, 400),
("PUT", "/api/v1/users/7", " " * ((1 << 20) + 1), 413),
("GET", "/api/v1/users?page=2&pageSize=10&tag=a&tag=b", None, 200),
("GET", "/api/v1/users?page=0", None, 400),
("DELETE", "/admin/v1/users/7", None, 401),
("POST", "/api/v1/newUsers", body, 201),
("POST", "/api/v1/acceptedUsers", body, 202),
]:
status, headers, data = request(port, method, path, payload, base)
assert status == expected, (method, path, status, data)
if path.startswith("/api/v1/users/7?") and status == 200:
assert json.loads(data)["data"] == {"userId": 7, "name": "Alice", "active": False}
assert json.loads(data)["requestId"] == "api-acceptance-42"
if path.startswith("/api/v1/users?page=2"):
assert json.loads(data)["data"]["page"] == 2
checks.extend(["mixedSources", "zeroAndFalse", "pathRange", "defaultAndQuery",
"requiredFields", "unknownFields", "duplicateScalar", "trailingJson",
"rangeValidation", "bodyLimit413", "auth401", "status201", "status202"])
assert request(port, "PUT", "/api/v1/users/7", body,
{"Content-Type": "text/plain"})[0] == 415
status, headers, data = request(port, "DELETE", "/admin/v1/users/7", headers={"Authorization": "Bearer test-only"})
assert status == 204 and data == b"" and headers.get("X-Audit") == "yes"
checks.extend(["contentType415", "middlewareComposition", "noContent204"])
process.send_signal(signal.SIGTERM)
assert process.wait(timeout=12) == 0
checks.append("gracefulShutdown")
try:
request(port, "GET", "/health/live")
raise AssertionError("listener survived shutdown")
except OSError:
checks.append("listenerReleased")
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=5)
if not starter:
logs = [json.loads(line) for line in log_path.read_text().splitlines() if line.startswith("{")]
assert any(row.get("msg") == "user.updated" and row.get("requestId") == "api-acceptance-42" for row in logs)
checks.append("businessLogCorrelation")
return {"project": "starter" if starter else "contract", "transport": transport,
"checks": checks, "passed": True}
def main():
started = time.monotonic()
results = []
with tempfile.TemporaryDirectory(prefix="goOmniApiAcceptance") as temp:
temp = Path(temp)
cli = temp / "go-omni"
run(["go", "build", "-o", cli, "./cmd/go-omni"])
for starter in (True, False):
project = temp / ("starter" if starter else "contract")
common = ["--module", "example.com/apiAcceptance", "--framework-dir", ROOT]
if starter:
run([cli, "api", "new", *common, project])
entry = project / "api/service.api"
else:
run([cli, "new", "--api", ROOT / "examples/contracts/user.api", *common, project])
entry = project / "api/user.api"
business = '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) UpdateUser(ctx context.Context,req *types.UpdateUserReq)(*types.UserResp,error){
s.deps.Logger.InfoContext(ctx,"user.updated","userId",req.UserID)
return &types.UserResp{UserID:req.UserID,Name:req.Name,Active:req.Active},nil
}
'''
write(project / "internal/service/user/updateUser.go", business)
write(project / "internal/service/user/listUsers.go", '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) ListUsers(ctx context.Context,req *types.ListUsersReq)(*types.ListUsersResp,error){return &types.ListUsersResp{Items:[]types.UserResp{},Page:req.Page,Total:0},nil}
''')
write(project / "internal/service/admin/user/deleteUser.go", '''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) DeleteUser(ctx context.Context,req *types.DeleteUserReq)error{return nil}
''')
write(project / "internal/middleware/auth.go", '''package middleware
import("git.yuntuo.hk/k2safe/go-omni/httpx";apierrors "git.yuntuo.hk/k2safe/go-omni/errors")
func Auth(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{if c.Header("Authorization")!="Bearer test-only"{return apierrors.Unauthorized("UNAUTHORIZED","authentication required")};return next(c)}}
''')
write(project / "internal/middleware/audit.go", '''package middleware
import("git.yuntuo.hk/k2safe/go-omni/httpx")
func Audit(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{c.SetResponseHeader("X-Audit","yes");return next(c)}}
''')
source = entry.read_text().replace(" UpdateUserReq {", ' CreateUserReq {\n Name string `json:"name"`\n Active bool `json:"active"`\n Age *int `json:"age,optional"`\n }\n UpdateUserReq {')
source += '''
@server (
group: user
prefix: /api/v1
)
service user-api {
@handler createUser
@status 201
post /newUsers (CreateUserReq) returns (UserResp)
@handler acceptUser
@status 202
post /acceptedUsers (CreateUserReq) returns (UserResp)
}
'''
entry.write_text(source)
run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"], ok=False)
run([cli, "api", "generate", "--api", entry, "--dir", project])
assert (project / "internal/service/user/updateUser.go").read_text() == business
for operation in ("CreateUser", "AcceptUser"):
name = operation[0].lower() + operation[1:]
write(project / f"internal/service/user/{name}.go", f'''package user
import("context";"example.com/apiAcceptance/internal/types")
func(s *Service) {operation}(ctx context.Context,req *types.CreateUserReq)(*types.UserResp,error){{return &types.UserResp{{UserID:8,Name:req.Name,Active:req.Active}},nil}}
''')
run([cli, "api", "validate", "--api", entry])
run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"])
binary = project / "server"
run(["go", "build", "-mod=mod", "-o", binary, "./cmd/server"], cwd=project)
for transport in ("nethttp", "native"):
result = exercise(binary, project, transport, starter)
results.append(result)
print(f"PASS {result['project']}/{transport}: {len(result['checks'])} checks", flush=True)
files = []
for folder in ("cmd/go-omni/apigen", "contract", "openapi", "examples/contracts"):
files.extend(p for p in (ROOT / folder).rglob("*") if p.is_file())
files.extend(ROOT / p for p in ("cmd/go-omni/api.go", "cmd/go-omni/main.go", "scripts/verifyAPI.py",
"transport/native/server.go", "transport/native/earlyReject_test.go"))
report = {"passed": True, "durationSeconds": round(time.monotonic() - started, 2),
"cases": results,
"sourceSha256": {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(files)}}
REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
print(f"Saved {REPORT}", flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""API + MySQL + storage business acceptance, using only disposable local resources.
--version installs the real remote CLI and tests consumers with no local replace.
No deployment is performed. Secrets and signed URLs are excluded from reports.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import secrets
import shutil
import signal
import socket
import subprocess
import tempfile
import time
import urllib.parse
import urllib.request
import urllib.error
from verifyDatabase import MySQL, waitFor
ROOT = Path(__file__).resolve().parents[1]
MODULE = "git.yuntuo.hk/k2safe/go-omni"
def run(args, cwd, env, timeout=180, expect_ok=True):
proc = subprocess.run([str(x) for x in args], cwd=cwd, env=env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout)
if (proc.returncode == 0) != expect_ok:
# Commands contain only paths and module versions, never credentials.
output = proc.stdout
for key, value in env.items():
if value and any(word in key for word in ("TOKEN", "PASSWORD", "SECRET", "DSN", "SIGNING_KEY")):
output = output.replace(value, "[REDACTED]")
raise RuntimeError(f"{args[0]} failed ({proc.returncode}):\n{output}")
return proc.stdout
class LimitedMySQL(MySQL):
def start(self):
endpoint = self.env.get("DOCKER_HOST") or run(
["docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}"], self.directory, self.env).strip()
if not endpoint.startswith(("unix://", "npipe://")):
raise RuntimeError("a local Docker context is required")
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0)); port = sock.getsockname()[1]
self.container = run(["docker", "run", "--detach", "--name", self.name,
"--label", "go-omni.test=apiWorkflow", "--cpus=1", "--memory=768m", "--pids-limit=256",
"--publish", f"127.0.0.1:{port}:3306", "--env", "MYSQL_ROOT_PASSWORD",
"--env", "MYSQL_PASSWORD", "--env", "MYSQL_DATABASE=omni_test", "--env", "MYSQL_USER=omni_test",
self.image], self.directory, self.env).strip()
self.wait()
self.dsn = f"omni_test:{self.password}@tcp(127.0.0.1:{port})/omni_test?parseTime=true&timeout=2s&readTimeout=2s&writeTimeout=2s"
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)
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,
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"
logs.parent.mkdir(parents=True, exist_ok=True)
# Keep each transport's business log checks separate.
logs.write_text("")
checks = []
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
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)
except urllib.error.HTTPError as exc: response = exc
with response:
data = response.read()
if response.status != expected: raise AssertionError(f"{method} {path}: expected {expected}, got {response.status}")
if response.headers.get("X-Request-ID") != "apiWorkflow": raise AssertionError("requestId header absent")
if not data: return None
value = json.loads(data)
if path != "/openapi.json" and value.get("requestId") != "apiWorkflow": raise AssertionError("requestId body absent")
return value
def start():
process = subprocess.Popen([str(binary)], cwd=project, env=environment, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def ready():
if process.poll() is not None: raise AssertionError("example failed startup")
try: return request("GET", "/health/ready")["code"] == "READY"
except (OSError, urllib.error.URLError): return False
try: waitFor(ready, "business service ready", 15)
except BaseException:
process.kill();process.wait(timeout=5);raise
return process
process = start()
try:
request("GET", "/api/v1/users", expected=401, auth=False); checks.append("authBeforeService")
empty = request("GET", "/api/v1/users")["data"]
assert empty["items"] == [] and empty["total"] == 0
checks.append("emptyPageAndQueryDefaults")
request("POST", "/api/v1/users", {"name": "X", "email": "invalid"}, 400)
request("POST", "/api/v1/users", {"name": "X", "email": "x@example.com", "unknown": 1}, 400)
request("GET", "/api/v1/users?pageSize=101", expected=400)
request("GET", "/api/v1/users/0", expected=400); checks.append("contractValidation")
user = request("POST", "/api/v1/users", {"name": " Alice ", "email": "Alice@Example.com"}, 201)["data"]
assert user["name"] == "Alice" and user["email"] == "alice@example.com"
uid = user["id"]; path = f"/api/v1/users/{uid}"
assert request("GET", path)["data"] == user; checks.append("createReadNormalize")
assert request("POST", "/api/v1/users", {"name": "Other", "email": "alice@example.com"}, 409)["code"] == "EMAIL_TAKEN"
checks.append("uniqueEmailConflict")
user = request("PUT", path, {"name": "Updated", "email": "updated@example.com"})["data"]
assert user["name"] == "Updated"
page = request("GET", "/api/v1/users?page=1&pageSize=1")["data"]
assert page["total"] == 1 and page["items"][0]["id"] == uid
checks.append("updateAndPagination")
# Force the second SQL write to fail: create/update/delete must roll back.
db.sql("CREATE TRIGGER reject_audit BEFORE INSERT ON user_audit FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='acceptance rollback';", "omni_test")
try:
request("POST", "/api/v1/users", {"name": "Rollback", "email": "rollback@example.com"}, 500)
request("PUT", path, {"name": "Rollback", "email": "rollback@example.com"}, 500)
request("DELETE", path, expected=500)
assert request("GET", path)["data"] == user
assert db.sql("SELECT COUNT(*) FROM users WHERE email='rollback@example.com'", "omni_test") == "0"
assert db.sql("SELECT COUNT(*) FROM user_audit", "omni_test") == "2"
finally: db.sql("DROP TRIGGER reject_audit", "omni_test")
checks.append("createUpdateDeleteRollbackOnAuditFailure")
link = request("POST", path + "/export")["data"]["downloadUrl"]
parsed = urllib.parse.urlparse(link)
assert parsed.hostname == "127.0.0.1" and parsed.port == port
with client.open(link, timeout=3) as response: exported = json.load(response)
assert exported == user; checks.append("storageUploadAndSignedDownload")
tampered = link.replace("signature=", "signature=bad")
if tampered == link:
query = urllib.parse.parse_qs(parsed.query)
key = next(k for k in query if k.lower() in ("sig", "signature"))
query[key] = ["bad"]
tampered = urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query, doseq=True)))
try: client.open(tampered, timeout=3); raise AssertionError("tampered signature accepted")
except urllib.error.HTTPError as exc: assert exc.code == 403
checks.append("signedDownloadTamperRejected")
doc = request("GET", "/openapi.json")
assert doc == json.loads((project / "docs/openapi.json").read_text()); checks.append("openAPIMatchesContract")
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()
waitFor(lambda: request("GET", "/health/ready")["code"] == "READY", "database recovery")
checks.append("readinessOutageAndRecovery")
process.send_signal(signal.SIGTERM); assert process.wait(timeout=12) == 0
process = start()
assert request("GET", path)["data"] == user; checks.append("persistenceAfterServiceRestart")
request("DELETE", path, expected=204)
assert request("GET", path, expected=404)["code"] == "USER_NOT_FOUND"
assert request("GET", "/api/v1/users")["data"]["items"] == []
assert db.sql("SELECT COUNT(*) FROM user_audit WHERE request_id='apiWorkflow'", "omni_test") == "3"
checks.append("deleteAndTransactionalAudit")
process.send_signal(signal.SIGTERM); assert process.wait(timeout=12) == 0
with socket.socket() as sock: assert sock.connect_ex(("127.0.0.1", port)) != 0
checks.append("gracefulStopAndListenerRelease")
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"))
checks.append("businessFileLogsAndSecretExclusion")
return {"transport": transport, "checks": checks}
finally:
if process.poll() is None: process.kill(); process.wait(timeout=5)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version")
parser.add_argument("--report", type=Path, default=ROOT / "performance/apiWorkflowLocal.json")
args = parser.parse_args()
env = {k: v for k, v in os.environ.items() if not k.startswith(("GO_OMNI_", "USER_SERVICE_"))}
env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk")
started = time.monotonic()
resolved = {}
with tempfile.TemporaryDirectory(prefix="goOmniAPIWorkflow-") as temp:
temp = Path(temp); cli = temp / "bin/go-omni"; cli.parent.mkdir()
if args.version:
env.update(GOBIN=str(cli.parent), GOMODCACHE=str(temp / "modcache"))
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env, timeout=300)
else: run(["go", "build", "-o", cli, "./cmd/go-omni"], ROOT, env)
project = temp / "userService"
source = ROOT / "examples/userService"
shutil.copytree(source, project, ignore=lambda directory, names: {"var", "server"} if Path(directory) == source else set())
if args.version:
run(["go", "mod", "edit", "-dropreplace=" + MODULE, "-require=" + MODULE + "@" + args.version], project, env)
else: run(["go", "mod", "edit", "-replace=" + MODULE + "=" + str(ROOT)], project, env)
run(["go", "mod", "tidy"], project, env)
if args.version:
assert "replace " not in (project / "go.mod").read_text()
modules = run(["go", "list", "-m", "-json", MODULE], project, env)
resolved = json.loads(modules)
assert resolved["Version"] == args.version and "Replace" not in resolved
# Installed CLI must pin its own version without a source checkout.
fresh = temp / "fresh"
run([cli, "api", "new", "--module", "example.com/fresh", "--database", "mysql", "--storage", "local", fresh], temp, env)
assert args.version in (fresh / "go.mod").read_text() and "replace " not in (fresh / "go.mod").read_text()
run(["go", "test", "-mod=mod", "./..."], fresh, env)
# Regeneration must preserve all developer-owned code and config.
manifest = json.loads((project / ".goOmni/apiManifest.json").read_text())
owned = set(manifest["generated"]) | {".goOmni/apiManifest.json"}
def hand_hashes():
return {str(p.relative_to(project)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in project.rglob("*") if p.is_file() and str(p.relative_to(project)) not in owned}
before = hand_hashes()
run([cli, "api", "generate", "--api", "api/user.api", "--dir", ".", "--check"], project, env)
run([cli, "api", "generate", "--api", "api/user.api", "--dir", "."], project, env)
assert before == hand_hashes()
db = LimitedMySQL(temp, env, "mysql:8.4")
try:
print("Starting bounded, isolated MySQL", flush=True); db.start()
env.update(GO_OMNI_DB_DSN=db.dsn, USER_SERVICE_TEST_DATABASE="1")
run([cli, "migrate", "--dir", "migrations", "--apply"], project, env)
run(["go", "test", "-race", "-count=1", "./..."], project, env)
run(["go", "vet", "./..."], project, env)
binary = project / "server";run(["go", "build", "-o", binary, "./cmd/server"], project, env)
groups = []
for transport in ("nethttp", "native"):
db.sql("DELETE FROM user_audit; DELETE FROM users;", "omni_test")
groups.append(exercise(binary, project, env, db, transport))
print("PASS business workflow: " + transport, flush=True)
finally: db.close()
version = args.version or "local"
report = {"passed": True, "frameworkVersion": version, "noLocalReplace": bool(args.version),
"durationSeconds": round(time.monotonic() - started, 2),
"regenerationPreservedHandwrittenFiles": len(before), "groups": groups,
"transactionTests": ["commit", "errorRollback", "panicRollback", "cancellationRollback"],
"startupTests": ["storageFailureClosesDatabase", "idempotentClose", "closedContextCannotReopen"],
"migrationCLI": True,
"frameworkSum": resolved.get("Sum"),
"sourceHashes": {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest()
for base in (ROOT / "cmd/go-omni", ROOT / "database", ROOT / "examples/userService")
for p in sorted(base.rglob("*")) if p.is_file() and p.suffix in (".go", ".tmpl", ".api", ".sql", ".yaml")}}
args.report.write_text(json.dumps(report, indent=2) + "\n")
print(f"PASS API workflow ({version}): {sum(len(g['checks']) for g in groups)} HTTP/lifecycle checks", flush=True)
if __name__ == "__main__":
def interrupt(signum, frame):
raise KeyboardInterrupt(f"signal {signum}")
for sig in (signal.SIGTERM, signal.SIGHUP): signal.signal(sig, interrupt)
main()

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Generate runnable logging/storage projects and verify their HTTP and shutdown paths."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import secrets
import signal
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cli', type=Path)
parser.add_argument('--framework-dir', type=Path, default=Path(__file__).resolve().parent.parent)
parser.add_argument('--output', type=Path, default=Path('performance/scaffoldModulesLocal.json'))
args = parser.parse_args()
root = args.framework_dir.resolve()
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
log_path = output.with_suffix('.txt')
env = {k: v for k, v in os.environ.items() if not k.startswith('GO_OMNI_')}
env.update(GOWORK='off', GOMAXPROCS='4')
report = {'status': 'running', 'startedAt': datetime.now(timezone.utc).isoformat(), 'cases': [], 'sourceSha256': {}}
sources = list((root / 'cmd/go-omni').rglob('*.go')) + list((root / 'cmd/go-omni/scaffold').rglob('*.tmpl'))
sources += [root / 'config/env.go', Path(__file__).resolve()]
for p in sorted(sources):
report['sourceSha256'][str(p.relative_to(root))] = hashlib.sha256(p.read_bytes()).hexdigest()
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def request(method, url, body=None, headers=None):
req = urllib.request.Request(url, data=body, headers=headers or {}, method=method)
try:
response = opener.open(req, timeout=3)
except urllib.error.HTTPError as error:
response = error
with response:
return response.status, response.read(), dict(response.headers)
def call(base, token, path, value=None, expected=200, method='POST'):
headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-Request-ID': 'scaffold-module-test'}
body = json.dumps(value).encode() if value is not None else None
status, data, _ = request(method, base + path, body, headers)
if status != expected:
raise AssertionError(f'{method} {path}: {status}, expected {expected}; {data[:512]!r}')
return json.loads(data) if data else None
failure = None
with log_path.open('w') as log, tempfile.TemporaryDirectory(prefix='goOmniModuleAcceptance') as directory:
temp = Path(directory).resolve()
def run(command, cwd):
subprocess.run(command, cwd=cwd, env=env, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180)
log.flush()
try:
cli = args.cli.resolve() if args.cli else temp / 'go-omni'
if not args.cli:
run(['go', 'build', '-o', str(cli), './cmd/go-omni'], root)
profiles = [('stderr', 'json', 'nethttp', signal.SIGTERM), ('stdout', 'text', 'native', signal.SIGINT),
('file', 'json', 'native', signal.SIGTERM), ('file', 'text', 'nethttp', signal.SIGINT)]
for index, (sink, fmt, transport, stop_signal) in enumerate(profiles):
print(f'Checking {sink}/{fmt}, {transport}, {stop_signal.name}', flush=True)
project = temp / ('case' + str(index))
run([str(cli), 'new', '--module', 'example.com/moduleAcceptance', '--framework-dir', str(root),
'--storage', 'local', '--log-output', sink, '--log-format', fmt, str(project)], temp)
run(['go', 'test', '-mod=mod', '-race', '-p=2', './...'], project)
binary = project / 'server'
run(['go', 'build', '-o', str(binary), './cmd/server'], project)
with socket.socket() as reservation:
reservation.bind(('127.0.0.1', 0))
port = reservation.getsockname()[1]
base = f'http://127.0.0.1:{port}'
token = secrets.token_hex(32)
runtime_env = dict(env, GO_OMNI_FILES_TOKEN=token, GO_OMNI_STORAGE_SIGNING_KEY=secrets.token_hex(32),
GO_OMNI_HTTP_ADDR=f'127.0.0.1:{port}', GO_OMNI_TRANSPORT=transport,
GO_OMNI_STORAGE_PUBLIC_URL=base + '/storage/object', GO_OMNI_STORAGE_MAX_BYTES='64')
process_log = project / 'process.log'
with process_log.open('w') as process_output:
process = subprocess.Popen([str(binary)], cwd=project, env=runtime_env, stdout=process_output, stderr=subprocess.STDOUT)
try:
deadline = time.monotonic() + 15
while True:
if process.poll() is not None:
raise RuntimeError('startup failed: ' + process_log.read_text())
try:
status, _, _ = request('GET', base + '/health/ready')
if status == 200:
break
except (OSError, urllib.error.URLError):
pass
if time.monotonic() > deadline:
raise TimeoutError('service readiness timeout')
time.sleep(0.05)
call(base, token, '/users', {'name': 'Example', 'email': 'module@example.com'}, 201)
data = b'hello go-omni'
upload_headers = {'Content-Type': 'text/plain', 'Authorization': 'Bearer ' + token, 'X-Request-ID': 'scaffold-module-test'}
status, raw, _ = request('POST', base + '/files', data, upload_headers)
if status != 201:
raise AssertionError(f'upload failed: {status}, {raw!r}')
key = json.loads(raw)['data']['key']
download = call(base, token, '/files/downloadLink', {'key': key})['data']['url']
status, raw, _ = request('GET', download)
if status != 200 or raw != data:
raise AssertionError('download content mismatch')
parts = urllib.parse.urlsplit(download)
query = urllib.parse.parse_qs(parts.query)
query['signature'] = ['tampered']
altered = urllib.parse.urlunsplit(parts._replace(query=urllib.parse.urlencode(query, doseq=True)))
if request('GET', altered)[0] != 403:
raise AssertionError('tampered signature accepted')
direct = call(base, token, '/files/uploadLink', {'size': len(data), 'contentType': 'text/plain'})['data']
signed = direct['request']
headers = {k: ', '.join(v) for k, v in signed['headers'].items()}
if request(signed['method'], signed['url'], data, headers)[0] != 201:
raise AssertionError('signed upload failed')
if request('POST', base + '/files', data, {'Content-Type': 'text/plain'})[0] != 401:
raise AssertionError('missing authorization accepted')
if request('POST', base + '/files', b'x' * 65, upload_headers)[0] != 413:
raise AssertionError('oversized upload accepted')
wrong_headers = dict(upload_headers, **{'Content-Type': 'image/png'})
if request('POST', base + '/files', data, wrong_headers)[0] != 415:
raise AssertionError('forged type accepted')
call(base, token, '/files/downloadLink', {'key': '../bad'}, 400)
call(base, token, '/files/downloadLink', {'key': 'missing'}, 404)
process.send_signal(stop_signal)
exit_code = process.wait(timeout=12)
if exit_code != 0:
raise AssertionError(f'{stop_signal.name}: exit {exit_code}')
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=5)
sink_path = project / 'var/log/server.log' if sink == 'file' else process_log
text = sink_path.read_text()
if token in text or 'signature=' in text or '"url":' in text:
raise AssertionError('secret or signed URL appeared in logs')
for event in ('user.created', 'file.uploaded', 'file.downloadLink.issued', 'file.uploadLink.issued', 'http.request', 'scaffold-module-test'):
if event not in text:
raise AssertionError('missing log evidence: ' + event)
if fmt == 'json':
entries = [json.loads(line) for line in text.splitlines() if line.strip()]
if not any(e.get('status') == 403 and e.get('path') == '/storage/object' for e in entries):
raise AssertionError('signed endpoint status lost in access logs')
with socket.socket() as check:
check.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
check.bind(('127.0.0.1', port))
# Startup validation must fail instead of silently exposing control endpoints.
invalid_env = dict(runtime_env, GO_OMNI_FILES_TOKEN='')
invalid = subprocess.run([str(binary)], cwd=project, env=invalid_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
if invalid.returncode == 0 or b'GO_OMNI_FILES_TOKEN' not in invalid.stdout:
raise AssertionError('missing token did not fail startup')
report['cases'].append({'logOutput': sink, 'logFormat': fmt, 'transport': transport, 'signal': stop_signal.name,
'status': 'passed', 'exitCode': exit_code, 'portReleased': True, 'missingTokenRejected': True})
report['status'] = 'passed'
except Exception as error:
failure = str(error)
report['status'], report['error'] = 'failed', failure
finally:
log.flush()
report['finishedAt'] = datetime.now(timezone.utc).isoformat()
report['logSha256'] = hashlib.sha256(log_path.read_bytes()).hexdigest()
output.write_text(json.dumps(report, indent=2) + '\n')
if failure:
raise SystemExit('FAIL: ' + failure + '; see ' + str(log_path))
print('PASS generated project HTTP/logging/shutdown; report: ' + str(output), flush=True)
if __name__ == '__main__':
main()

95
scripts/verifyUploads.py Normal file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Run real S3 uploads in one bounded, network-isolated local Docker container."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import uuid
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--image', default='quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z')
parser.add_argument('--output', type=Path, default=Path('performance/uploadsLocal.json'))
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
output = args.output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
log_path = output.with_suffix('.txt')
name = 'go-omni-upload-' + uuid.uuid4().hex[:12]
def capture(command):
return subprocess.check_output(command, text=True, timeout=15).strip()
arch = capture(['docker', 'info', '--format', '{{.Architecture}}'])
arch = {'aarch64': 'arm64', 'x86_64': 'amd64'}.get(arch, arch)
if arch not in ('arm64', 'amd64'):
raise SystemExit('unsupported Docker architecture: ' + arch)
before = set(capture(['docker', 'ps', '-q']).split())
report = {
'status': 'running', 'startedAt': datetime.now(timezone.utc).isoformat(),
'architecture': arch, 'goVersion': capture(['go', 'version']),
'image': args.image, 'imageId': capture(['docker', 'image', 'inspect', args.image, '--format', '{{.Id}}']),
'sourceSha256': {}, 'commands': [],
'containerLimits': {'cpus': 2, 'memoryBytes': 2147483648, 'pids': 256, 'network': 'none'},
'notes': ['MinIO verifies S3 signatures; OSS/R2 real cloud accounts are not exercised.'],
}
for p in sorted(list((root / 'storage').rglob('*.go')) + list((root / 'internal/storagefixture').glob('*.go')) + [root / 'go.mod', root / 'go.sum']):
report['sourceSha256'][str(p.relative_to(root))] = hashlib.sha256(p.read_bytes()).hexdigest()
failure = None
with log_path.open('w') as log:
try:
with tempfile.TemporaryDirectory(prefix='goOmniUpload') as temporary:
Path(temporary).chmod(0o755)
env = dict(os.environ, GOOS='linux', GOARCH=arch, CGO_ENABLED='0', GOMAXPROCS='2', GOWORK='off')
build = ['go', 'test', '-p=2', '-c', '-o', temporary + '/s3.test', './storage/s3']
report['commands'].append(build)
subprocess.run(build, cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=240)
check = '''set -eu
minio --version
minio server /data --address 127.0.0.1:9000 --console-address 127.0.0.1:9001 >/tmp/minio.log 2>&1 &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true' EXIT
/test/s3.test -test.v -test.run '^TestS3Integration$' -test.count=1 -test.timeout=60s || { cat /tmp/minio.log; exit 1; }
'''
command = ['docker', 'run', '--rm', '--name', name, '--platform', 'linux/' + arch,
'--cpus', '2', '--memory', '2048m', '--memory-swap', '2048m', '--pids-limit', '256',
'--network', 'none', '--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--user', '1000:1000', '--tmpfs', '/tmp:rw,nosuid,noexec,size=64m,mode=1777',
'--tmpfs', '/data:rw,nosuid,noexec,size=128m,mode=1777', '-v', temporary + ':/test:ro',
'-e', 'GOMAXPROCS=2', '-e', 'MINIO_ROOT_USER=goOmniTest', '-e', 'MINIO_ROOT_PASSWORD=goOmniTestSecret',
'-e', 'GO_OMNI_S3_TEST_ADDR=127.0.0.1:9000', '--entrypoint', 'sh', args.image, '-c', check]
report['commands'].append(command)
subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=90)
log.flush()
if '--- PASS: TestS3Integration' not in log_path.read_text():
raise RuntimeError('missing integration pass evidence')
report['status'] = 'passed'
except Exception as error:
failure = str(error)
report['status'], report['error'] = 'failed', failure
finally:
remaining = subprocess.run(['docker', 'inspect', name], capture_output=True, timeout=15)
if remaining.returncode == 0:
subprocess.run(['docker', 'stop', '-t', '3', name], capture_output=True, timeout=15, check=True)
after = set(capture(['docker', 'ps', '-q']).split())
report['preExistingContainersStillRunning'] = before.issubset(after)
if not before.issubset(after):
failure = 'a pre-existing container is no longer running'
report['status'], report['error'] = 'failed', failure
report['cleanedContainer'] = name
report['finishedAt'] = datetime.now(timezone.utc).isoformat()
log.flush()
report['logSha256'] = hashlib.sha256(log_path.read_bytes()).hexdigest()
output.write_text(json.dumps(report, indent=2) + '\n')
if failure:
raise SystemExit('FAIL: ' + failure + '; see ' + str(log_path))
print('PASS real S3 direct/multipart uploads, pagination, abort, signed constraints; report: ' + str(output))
if __name__ == '__main__':
main()