138 lines
6.4 KiB
Python
Executable File
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()
|