The CI-contract test now checks the Gitea workflow smokes the Maven final jar (the orb .qqq-ci.yml is gone). The release-bundle test renders compose.yaml with docker compose config, so CI installs a checksum-pinned Compose binary behind a docker compose-only shim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
536 lines
25 KiB
Python
536 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Contract tests for the HTTP smoke checks, without starting the application."""
|
|
|
|
import http.cookiejar
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import socketserver
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import unittest
|
|
import urllib.error
|
|
import urllib.request
|
|
import xml.etree.ElementTree as ET
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from smoke import SmokeClient, check_artemis_round_trip, check_core, check_rabbitmq, check_tables, check_runtime_features, CORE_TABLES
|
|
import run_full_smoke
|
|
from run_full_smoke import ROOT
|
|
|
|
|
|
class SmokeServer(ThreadingHTTPServer):
|
|
def server_bind(self):
|
|
socketserver.TCPServer.server_bind(self)
|
|
self.server_name = "localhost"
|
|
self.server_port = self.server_address[1]
|
|
|
|
|
|
class DemoHandler(BaseHTTPRequestHandler):
|
|
orders = 0
|
|
product_name = "QRun Starter Kit"
|
|
trace_label = "Sync Order - demo"
|
|
trace_order_id = None
|
|
inserted_marker = None
|
|
|
|
def log_message(self, *_args):
|
|
pass
|
|
|
|
def do_GET(self):
|
|
if self.path == "/data/apiCatalog/103":
|
|
return self.send_json({"values": {"id": 103, "name": "Shipping Box"}})
|
|
if self.path == "/health":
|
|
return self.send_json({"status": "UP"})
|
|
if self.path == "/":
|
|
body = b'<html><script src="/_next/static/chunks/demo.js"></script></html>'
|
|
self.send_response(200)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
return
|
|
if self.path == "/_next/static/chunks/demo.js":
|
|
body = b"console.log('demo');"
|
|
self.send_response(200)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
return
|
|
self.send_error(404)
|
|
|
|
def do_POST(self):
|
|
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
if self.path == "/data/apiCatalog/":
|
|
return self.send_error(403)
|
|
if self.path.startswith("/processes/calculateOrderTotal/run"):
|
|
if "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" not in self.headers.get("Cookie", ""):
|
|
return self.send_error(403)
|
|
return self.send_json({"values": {"totalCents": 3750}})
|
|
if self.path == "/data/order/":
|
|
assert body["orderNo"].startswith("SMOKE-")
|
|
type(self).orders += 1
|
|
type(self).trace_order_id = 100 + self.orders
|
|
type(self).inserted_marker = body["orderNo"]
|
|
return self.send_json({"records": [{"values": {"id": 100 + self.orders}}]})
|
|
if self.path.startswith("/qqq/v1/table/") and self.path.endswith("/query"):
|
|
table = self.path.split("/")[4]
|
|
if table == "apiCatalog":
|
|
return self.send_json({"records": [{"values": {"id": i}} for i in (101, 102, 103)]})
|
|
if table == "processTrace":
|
|
records = [{"recordLabel": self.trace_label,
|
|
"values": {"id": i, "processUUID": f"trace-{i}",
|
|
"keyRecordId": self.trace_order_id}}
|
|
for i in range(self.orders)]
|
|
else:
|
|
values = {
|
|
"demoNote": {"id": 1, "text": "Transient demo note"},
|
|
"customer": {"id": 1, "name": "Ada Lovelace"},
|
|
"order": {"id": 1, "orderNo": "ORD-1001"},
|
|
"orderLine": {"id": 1, "quantity": 2},
|
|
"product": {"id": 1, "name": self.product_name},
|
|
}
|
|
records = [{"values": values.get(table, {"id": 1})}]
|
|
if table == "order" and self.inserted_marker:
|
|
records.append({"values": {"id": 100 + self.orders,
|
|
"orderNo": self.inserted_marker}})
|
|
return self.send_json({"records": records})
|
|
self.send_error(404)
|
|
|
|
def do_PATCH(self):
|
|
if self.path == "/data/demoNote/1":
|
|
return self.send_error(403)
|
|
self.send_error(404)
|
|
|
|
def send_json(self, value):
|
|
data = json.dumps(value).encode()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
|
|
|
|
class SmokeTest(unittest.TestCase):
|
|
def setUp(self):
|
|
DemoHandler.orders = 0
|
|
DemoHandler.trace_order_id = None
|
|
DemoHandler.inserted_marker = None
|
|
|
|
def test_catalog_missing_final_page_fails(self):
|
|
client = mock.Mock()
|
|
client.records.return_value = [{"values": {"id": value}} for value in (101, 102)]
|
|
with self.assertRaisesRegex(AssertionError, "all pages"):
|
|
check_runtime_features(client)
|
|
|
|
def test_wrong_javascript_result_fails(self):
|
|
client = mock.Mock()
|
|
client.records.return_value = [{"values": {"id": value}} for value in (101, 102, 103)]
|
|
client.json.side_effect = [{"values": {"name": "Shipping Box"}}, {"values": {"totalCents": 3749}}]
|
|
with self.assertRaisesRegex(AssertionError, "incorrect total"):
|
|
check_runtime_features(client)
|
|
|
|
def test_core_runner_fails_promptly_when_jar_exits(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
jar = Path(directory) / "broken.jar"
|
|
jar.write_bytes(b"not a jar")
|
|
result = subprocess.run(
|
|
[sys.executable, str(Path(__file__).with_name("run_core_smoke.py")),
|
|
str(jar), "--timeout", "1"], capture_output=True, text=True, timeout=5,
|
|
)
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("packaged app exited", result.stderr)
|
|
|
|
def test_ci_core_smokes_maven_final_artifact(self):
|
|
pom = ET.parse(ROOT / "qqq-all-app" / "pom.xml")
|
|
final_name = pom.findtext(".//{http://maven.apache.org/POM/4.0.0}build/"
|
|
"{http://maven.apache.org/POM/4.0.0}finalName")
|
|
# CI smokes the exact jar Maven builds; keep the build output and the
|
|
# CI smoke command on the same artifact path.
|
|
self.assertEqual("qqq-all-app", final_name)
|
|
workflow = (ROOT / ".gitea" / "workflows" / "build.yaml").read_text()
|
|
self.assertIn("python3 scripts/run_core_smoke.py qqq-all-app/target/qqq-all-app.jar", workflow)
|
|
|
|
|
|
def test_standalone_full_smoke_requires_explicit_search_endpoint(self):
|
|
result = subprocess.run(
|
|
[sys.executable, str(Path(__file__).with_name("smoke.py")), "full",
|
|
"--base-url", "http://127.0.0.1:1"],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("run_full_smoke.py", result.stderr)
|
|
|
|
def test_full_runner_reserves_distinct_non_ephemeral_app_ports(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / ".env.example").write_text(
|
|
"QQQ_ALL_PORT=8080\nKEYCLOAK_PORT=8081\nPOSTGRES_PASSWORD=example\n")
|
|
with mock.patch.object(run_full_smoke, "ROOT", root):
|
|
values = run_full_smoke.demo_environment()
|
|
app_port = int(values["QQQ_ALL_PORT"])
|
|
keycloak_port = int(values["KEYCLOAK_PORT"])
|
|
self.assertTrue(20000 <= app_port < 30000)
|
|
self.assertTrue(20000 <= keycloak_port < 30000)
|
|
self.assertNotEqual(app_port, keycloak_port)
|
|
self.assertNotEqual("example", values["POSTGRES_PASSWORD"])
|
|
|
|
def test_full_runner_supplies_generated_credentials_to_http_checks(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "compose.yaml").write_text("services: {}\n")
|
|
values = {"QQQ_ALL_PORT": "20001", "KEYCLOAK_PORT": "20002",
|
|
"DEMO_USER_PASSWORD": "generated-user",
|
|
"DEMO_ADMIN_PASSWORD": "generated-admin",
|
|
"RABBITMQ_PASSWORD": "generated-rabbit"}
|
|
|
|
def verify_credentials(*_args):
|
|
for key in ("DEMO_USER_PASSWORD", "DEMO_ADMIN_PASSWORD", "RABBITMQ_PASSWORD"):
|
|
self.assertEqual(values[key], os.environ[key])
|
|
|
|
with (mock.patch.object(run_full_smoke, "ROOT", root),
|
|
mock.patch.object(run_full_smoke, "demo_environment", return_value=values),
|
|
mock.patch.object(run_full_smoke, "published_port", return_value=20003),
|
|
mock.patch.object(run_full_smoke.Diagnostics, "pull"),
|
|
mock.patch.object(run_full_smoke.subprocess, "run",
|
|
return_value=subprocess.CompletedProcess([], 0)),
|
|
mock.patch.object(run_full_smoke, "check_full", side_effect=verify_credentials) as check,
|
|
mock.patch.dict(os.environ, {}, clear=False)):
|
|
run_full_smoke.main()
|
|
check.assert_called_once()
|
|
|
|
def test_full_runner_queries_private_opensearch_service(self):
|
|
command = ["docker", "compose", "-p", "smoke"]
|
|
with mock.patch.object(run_full_smoke.subprocess, "run",
|
|
return_value=subprocess.CompletedProcess([], 0,
|
|
stdout='{"hits":{"hits":[]}}')) as invoke:
|
|
result = run_full_smoke.query_opensearch(command, {}, "customers", {"query": {}})
|
|
self.assertEqual([], result["hits"]["hits"])
|
|
args = invoke.call_args.args[0]
|
|
self.assertEqual(["exec", "-T", "opensearch", "curl"], args[len(command):len(command) + 4])
|
|
self.assertIn("http://127.0.0.1:9200/customers/_search", args)
|
|
|
|
def test_full_runner_fails_when_private_opensearch_query_fails(self):
|
|
with mock.patch.object(run_full_smoke.subprocess, "run",
|
|
return_value=subprocess.CompletedProcess([], 22, stdout="")):
|
|
with self.assertRaisesRegex(AssertionError, "OpenSearch query failed"):
|
|
run_full_smoke.query_opensearch(["docker", "compose"], {}, "customers", {"query": {}})
|
|
|
|
def test_full_runner_prepares_sftp_import_directory(self):
|
|
command = ["docker", "compose", "-p", "smoke"]
|
|
with mock.patch.object(run_full_smoke.subprocess, "run",
|
|
return_value=subprocess.CompletedProcess([], 0)) as invoke:
|
|
run_full_smoke.prepare_sftp_import_directory(command, {}, "qqq")
|
|
self.assertEqual([
|
|
command + ["exec", "-T", "sftp", "mkdir", "-p", "/home/qqq/upload/imports"],
|
|
command + ["exec", "-T", "sftp", "chown", "1001:1001", "/home/qqq/upload/imports"],
|
|
], [call.args[0] for call in invoke.call_args_list])
|
|
|
|
def test_full_runner_fails_when_sftp_fixture_cannot_be_prepared(self):
|
|
with mock.patch.object(run_full_smoke.subprocess, "run", side_effect=[
|
|
subprocess.CompletedProcess([], 0), subprocess.CompletedProcess([], 1)]):
|
|
with self.assertRaisesRegex(AssertionError, "SFTP import directory"):
|
|
run_full_smoke.prepare_sftp_import_directory(["docker", "compose"], {}, "qqq")
|
|
|
|
def test_oidc_cookie_is_sent_only_to_loopback_http_app(self):
|
|
from smoke import allow_loopback_http_session, require_oidc_session
|
|
|
|
class SecureSession(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path in ("/login", "/login-plain"):
|
|
data = b"ok"
|
|
self.send_response(200)
|
|
secure = "; Secure" if self.path == "/login" else ""
|
|
self.send_header("Set-Cookie", "sessionUUID=test-session; Path=/; HttpOnly" + secure)
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
return
|
|
if self.path == "/protected":
|
|
if "sessionUUID=test-session" not in self.headers.get("Cookie", ""):
|
|
return self.send_error(403)
|
|
return self.send_json({"ok": True})
|
|
return super().do_GET()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), SecureSession)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "HttpOnly OIDC session"):
|
|
require_oidc_session(client, {})
|
|
client.request("/login")
|
|
require_oidc_session(client, {})
|
|
with self.assertRaisesRegex(AssertionError, "exposed"):
|
|
require_oidc_session(client, {"uuid": "test-session"})
|
|
cookie = next(iter(client.cookies))
|
|
cookie._rest.clear()
|
|
with self.assertRaisesRegex(AssertionError, "HttpOnly OIDC session"):
|
|
require_oidc_session(client, {})
|
|
cookie.set_nonstandard_attr("HttpOnly", None)
|
|
with self.assertRaisesRegex(AssertionError, "HTTP 403"):
|
|
client.request("/protected")
|
|
allow_loopback_http_session(client)
|
|
self.assertTrue(client.json("/protected")["ok"])
|
|
plain = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
plain.request("/login-plain")
|
|
allow_loopback_http_session(plain)
|
|
self.assertTrue(plain.json("/protected")["ok"])
|
|
remote = SmokeClient("http://example.com")
|
|
with self.assertRaisesRegex(AssertionError, "loopback"):
|
|
allow_loopback_http_session(remote)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_keycloak_secure_auth_cookie_is_sent_on_loopback_http(self):
|
|
from smoke import allow_loopback_http_cookies
|
|
|
|
class SecureKeycloak(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path == "/auth":
|
|
data = b"login"
|
|
self.send_response(200)
|
|
self.send_header("Set-Cookie", "AUTH_SESSION_ID=test-auth; Path=/; Secure; HttpOnly")
|
|
self.send_header("Content-Length", str(len(data)))
|
|
self.end_headers()
|
|
self.wfile.write(data)
|
|
return
|
|
return super().do_GET()
|
|
|
|
def do_POST(self):
|
|
if self.path == "/login":
|
|
if "AUTH_SESSION_ID=test-auth" not in self.headers.get("Cookie", ""):
|
|
return self.send_error(400)
|
|
return self.send_json({"ok": True})
|
|
return super().do_POST()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), SecureKeycloak)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
base = f"http://127.0.0.1:{server.server_port}"
|
|
cookies = http.cookiejar.CookieJar()
|
|
browser = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookies))
|
|
browser.open(base + "/auth", timeout=2).close()
|
|
with self.assertRaises(urllib.error.HTTPError) as denied:
|
|
browser.open(urllib.request.Request(base + "/login", data=b"x"), timeout=2)
|
|
denied.exception.close()
|
|
allow_loopback_http_cookies(cookies, base)
|
|
with browser.open(urllib.request.Request(base + "/login", data=b"x"), timeout=2) as response:
|
|
self.assertEqual({"ok": True}, json.load(response))
|
|
with self.assertRaisesRegex(AssertionError, "loopback"):
|
|
allow_loopback_http_cookies(cookies, "http://example.com")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_core_checks_public_http_behavior(self):
|
|
server = SmokeServer(("127.0.0.1", 0), DemoHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
client.session_id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
|
check_core(client, timeout=2)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_missing_backend_records_fail(self):
|
|
server = SmokeServer(("127.0.0.1", 0), DemoHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "missing expected customer"):
|
|
client.require_record("customer", "name", "Grace Hopper")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_core_rejects_wrong_filesystem_seed(self):
|
|
class WrongProduct(DemoHandler):
|
|
product_name = "Unrelated Product"
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), WrongProduct)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "missing expected product"):
|
|
check_tables(client, CORE_TABLES)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_artemis_rejects_unrelated_trace(self):
|
|
class WrongTrace(DemoHandler):
|
|
trace_label = "Unrelated Process - demo"
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), WrongTrace)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "Artemis.*syncOrder"):
|
|
check_artemis_round_trip(client, timeout=0.3)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_artemis_returns_the_new_trace_uuid(self):
|
|
server = SmokeServer(("127.0.0.1", 0), DemoHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
self.assertEqual("trace-0", check_artemis_round_trip(client, timeout=2))
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_artemis_rejects_trace_for_another_order(self):
|
|
class WrongOrderTrace(DemoHandler):
|
|
trace_order_id = 999
|
|
|
|
def do_POST(self):
|
|
if self.path == "/data/order/":
|
|
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
assert body["orderNo"].startswith("SMOKE-")
|
|
type(self).orders += 1
|
|
return self.send_json({"records": [{"values": {"id": 101}}]})
|
|
return super().do_POST()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), WrongOrderTrace)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "Artemis.*syncOrder"):
|
|
check_artemis_round_trip(client, timeout=0.3)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_rabbitmq_rejects_unrelated_completion(self):
|
|
class WrongCompletion(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path == "/qqq/v1/esb/messages/orderSyncEvents":
|
|
event = {"type": "qqq.process.other.completed",
|
|
"data": {"processName": "other", "processUUID": "trace-1"}}
|
|
return self.send_json({"messages": [{"event": event}], "hasMore": False})
|
|
return super().do_GET()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), WrongCompletion)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "RabbitMQ syncOrder completion"):
|
|
check_rabbitmq(client, 0.3, "trace-1")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_rabbitmq_rejects_wrong_process_uuid(self):
|
|
class WrongRun(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path == "/qqq/v1/esb/messages/orderSyncEvents":
|
|
event = {"type": "qqq.process.syncOrder.completed",
|
|
"data": {"processName": "syncOrder", "processUUID": "other-run"}}
|
|
return self.send_json({"messages": [{"event": event}], "hasMore": False})
|
|
return super().do_GET()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), WrongRun)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
with self.assertRaisesRegex(AssertionError, "RabbitMQ syncOrder completion"):
|
|
check_rabbitmq(client, 0.3, "trace-0")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_rabbitmq_accepts_matching_process_uuid(self):
|
|
class MatchingRun(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path == "/qqq/v1/esb/messages/orderSyncEvents":
|
|
event = {"type": "qqq.process.syncOrder.completed",
|
|
"data": {"processName": "syncOrder", "processUUID": "trace-0"}}
|
|
return self.send_json({"messages": [{"event": event}], "hasMore": False})
|
|
return super().do_GET()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), MatchingRun)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
check_rabbitmq(client, 2, "trace-0")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
def test_rabbitmq_finds_completion_after_first_browse_page(self):
|
|
class LaterPage(DemoHandler):
|
|
def do_GET(self):
|
|
if self.path == "/qqq/v1/esb/messages/orderSyncEvents":
|
|
old = {"type": "qqq.process.syncOrder.completed",
|
|
"data": {"processName": "syncOrder", "processUUID": "old-run"}}
|
|
return self.send_json({"messages": [{"event": old}] * 50, "hasMore": True})
|
|
if self.path == "/qqq/v1/esb/messages/orderSyncEvents?offset=50":
|
|
event = {"type": "qqq.process.syncOrder.completed",
|
|
"data": {"processName": "syncOrder", "processUUID": "trace-0"}}
|
|
return self.send_json({"messages": [{"event": event}], "hasMore": False})
|
|
return super().do_GET()
|
|
|
|
server = SmokeServer(("127.0.0.1", 0), LaterPage)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
client = SmokeClient(f"http://127.0.0.1:{server.server_port}")
|
|
check_rabbitmq(client, 2, "trace-0")
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
@unittest.skipUnless((ROOT / "compose.yaml").is_file() and shutil.which("docker"),
|
|
"C6 Compose integration is not available")
|
|
def test_full_smoke_keeps_opensearch_private_and_uses_ephemeral_service_ports(self):
|
|
command = ["docker", "compose", "--env-file", str(ROOT / ".env.example"),
|
|
"-f", str(ROOT / "compose.yaml"),
|
|
"-f", str(ROOT / "scripts/compose.smoke.yaml"),
|
|
"--profile", "full", "config", "--format", "json"]
|
|
result = subprocess.run(command, capture_output=True, text=True, timeout=15)
|
|
self.assertEqual(0, result.returncode, "full smoke Compose config failed")
|
|
services = json.loads(result.stdout)["services"]
|
|
self.assertNotIn("ports", services["opensearch"])
|
|
for service, target in (("rabbitmq", 15672),
|
|
("artemis", 8161), ("mailpit", 8025), ("minio", 9001)):
|
|
with self.subTest(service=service):
|
|
ports = services[service]["ports"]
|
|
self.assertEqual(1, len(ports))
|
|
self.assertEqual(target, ports[0]["target"])
|
|
self.assertEqual("127.0.0.1", ports[0]["host_ip"])
|
|
self.assertFalse(ports[0].get("published"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|