test: verify remote business module upgrade flow
This commit is contained in:
137
scripts/verifyBusiness.py
Executable file
137
scripts/verifyBusiness.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/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()
|
||||
@@ -145,7 +145,7 @@ class Server:
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--go", default="go")
|
||||
parser.add_argument("--version", default="v0.3.0")
|
||||
parser.add_argument("--version", default="v0.3.1")
|
||||
parser.add_argument("--cli", help="Use a locally built CLI")
|
||||
parser.add_argument("--framework-dir", help="Local framework source for pre-release checks")
|
||||
parser.add_argument("--image", default="mysql:8.4")
|
||||
|
||||
@@ -91,7 +91,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--go", default="go", help="Go executable")
|
||||
parser.add_argument("--version", default="v0.3.0", help="Released CLI version to install")
|
||||
parser.add_argument("--version", default="v0.3.1", help="Released CLI version to install")
|
||||
parser.add_argument("--cli", help="Use a locally built CLI instead of installing")
|
||||
parser.add_argument("--framework-version", help="Explicit version passed to the CLI")
|
||||
parser.add_argument("--framework-dir", help="Local checkout for pre-release verification")
|
||||
|
||||
@@ -13,7 +13,7 @@ from verifyScaffold import MODULE, run
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--go", default="go")
|
||||
parser.add_argument("--version", default="v0.3.0")
|
||||
parser.add_argument("--version", default="v0.3.1")
|
||||
parser.add_argument("--framework-dir", help="Use a local checkout before release")
|
||||
args = parser.parse_args()
|
||||
env = dict(os.environ, GOWORK="off")
|
||||
|
||||
Reference in New Issue
Block a user