Files
go-omni/scripts/verifyBusiness.py

138 lines
6.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Verify a real generated business module across a released framework upgrade."""
import argparse
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
from verifyScaffold import MODULE, run
def free_address():
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
return f"127.0.0.1:{listener.getsockname()[1]}"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--go", default="go")
parser.add_argument("--version", default="v0.3.1", help="Target framework release")
parser.add_argument("--from-version", default="v0.2.0", help="Version used before upgrade")
args = parser.parse_args()
env = dict(os.environ, GOWORK="off")
env["GOPRIVATE"] = ",".join(filter(None, [env.get("GOPRIVATE", ""), "git.yuntuo.hk"]))
with tempfile.TemporaryDirectory(prefix="go omni business ") as temporary:
root = Path(temporary).resolve()
env["GOBIN"] = str(root / "bin")
print(f"Installing released CLI {args.version}", flush=True)
run([args.go, "install", f"{MODULE}/cmd/go-omni@{args.version}"], root, env)
cli = root / "bin" / "go-omni"
project = root / "billing service"
run([
str(cli), "new", "--database", "memory", "--framework-version",
args.from_version, "--module", "example.com/billing", str(project),
], root, env)
run([args.go, "mod", "tidy"], project, env)
manifest = (project / "go.mod").read_text()
if f"{MODULE} {args.from_version}" not in manifest or "replace " in manifest:
raise AssertionError("initial business project was not pinned to the expected remote version")
run([args.go, "test", "-race", "./..."], project, env)
run([args.go, "get", f"{MODULE}@{args.version}"], project, env)
run([args.go, "mod", "tidy"], project, env)
upgraded = (project / "go.mod").read_text()
if f"{MODULE} {args.version}" not in upgraded or "replace " in upgraded:
raise AssertionError("business project upgrade did not produce a clean remote pin")
run([args.go, "test", "-race", "./..."], project, env)
binary = project / "billing-server"
run([args.go, "build", "-o", str(binary), "./cmd/server"], project, env)
address = free_address()
log_file = project / "business.log"
server_env = dict(env)
server_env.update(
GO_OMNI_HTTP_ADDR=address,
GO_OMNI_MODE="release",
GO_OMNI_ENABLE_LOGGER="true",
GO_OMNI_LOG_FILE=str(log_file),
GO_OMNI_LOG_FORMAT="json",
GO_OMNI_SHUTDOWN_TIMEOUT="2s",
)
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def request(method, path, body=None, request_id="business-request"):
payload = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(
f"http://{address}{path}", data=payload, method=method,
headers={"Content-Type": "application/json", "X-Request-ID": request_id},
)
try:
with client.open(req, timeout=3) as response:
return response.status, response.headers, json.load(response)
except urllib.error.HTTPError as error:
return error.code, error.headers, json.loads(error.read())
process = subprocess.Popen(
[str(binary)], cwd=project, env=server_env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
)
try:
deadline = time.monotonic() + 15
while True:
if process.poll() is not None:
output = process.stdout.read() if process.stdout else ""
raise RuntimeError(f"business service exited during startup: {output}")
try:
status, _, _ = request("GET", "/health/live")
if status == 200:
break
except OSError:
pass
if time.monotonic() >= deadline:
raise RuntimeError("business service did not start")
time.sleep(0.05)
status, headers, body = request("GET", "/health/ready")
if status != 200 or body.get("code") != "READY":
raise AssertionError(f"readiness failed: {status} {body}")
status, headers, body = request("POST", "/users", {"name": "broken"}, "invalid-request")
if status != 400 or body.get("code") != "INVALID_BODY" or headers.get("X-Request-ID") != "invalid-request":
raise AssertionError(f"validation contract failed: {status} {body}")
status, _, body = request("POST", "/users", {"name": "Alice", "email": "alice@example.com"}, "create-request")
if status != 201 or body.get("data", {}).get("email") != "alice@example.com":
raise AssertionError(f"business create failed: {status} {body}")
status, _, body = request("POST", "/users", {"name": "Duplicate", "email": "ALICE@example.com"}, "duplicate-request")
if status != 409 or body.get("code") != "EMAIL_TAKEN":
raise AssertionError(f"business error mapping failed: {status} {body}")
process.send_signal(signal.SIGTERM)
if process.wait(timeout=8) != 0:
raise AssertionError("business service did not exit cleanly")
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=5)
content = log_file.read_text()
if '"http.request"' not in content or '"requestId":"create-request"' not in content:
raise AssertionError(f"access log did not contain business request correlation: {content}")
if '"service":"example.com/billing"' not in content:
raise AssertionError(f"business service name was not attached to logs: {content}")
if "private" in content.lower() and "password" in content.lower():
raise AssertionError("sensitive text leaked into business logs")
print(f"PASS business module: {args.from_version} -> {args.version}, clean remote pin, CRUD, validation, errors, requestId and logs", flush=True)
if __name__ == "__main__":
main()