From 4956870a0493fe1ae75a4a6e1421e31016b45196 Mon Sep 17 00:00:00 2001 From: chestercheng Date: Wed, 12 Aug 2026 23:42:08 +0800 Subject: [PATCH 1/4] feat: add PostgreSQL database support Install the psycopg driver and configure PostgreSQL connection timeouts. Run the same clean-image API flow against SQLite and PostgreSQL. Signed-off-by: chestercheng --- README.md | 2 +- pyproject.toml | 1 + src/argus/database.py | 3 + tests/test_docker_integration.py | 229 +++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 tests/test_docker_integration.py diff --git a/README.md b/README.md index 8f0fba3..0df73d3 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ uv sync --group dev # create .venv and install all dependencies set -a && source .env && set +a uv run uvicorn argus.main:app --host 0.0.0.0 --port 8000 # start server -uv run pytest tests/ # run automated tests +uv run pytest tests/ # run tests, including clean Docker builds uv run ruff check src tests # lint uv run ruff format src tests # format diff --git a/pyproject.toml b/pyproject.toml index 5e3c297..476ebaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "authlib<1.7", "itsdangerous", "jinja2", + "psycopg[binary]", "sqlalchemy", ] diff --git a/src/argus/database.py b/src/argus/database.py index b931abb..5117115 100644 --- a/src/argus/database.py +++ b/src/argus/database.py @@ -2,6 +2,7 @@ from collections.abc import Iterator from contextlib import contextmanager +import math from sqlalchemy import ForeignKey, Integer, String, create_engine, text from sqlalchemy.engine import Engine @@ -89,6 +90,8 @@ def create_db_engine(database_url: str, connect_timeout: float | None = None) -> backend = make_url(database_url).get_backend_name() if backend == "sqlite": connect_args["timeout"] = connect_timeout + elif backend == "postgresql": + connect_args["connect_timeout"] = max(1, math.ceil(connect_timeout)) return create_engine(database_url, connect_args=connect_args) diff --git a/tests/test_docker_integration.py b/tests/test_docker_integration.py new file mode 100644 index 0000000..d85c9d3 --- /dev/null +++ b/tests/test_docker_integration.py @@ -0,0 +1,229 @@ +from base64 import b64encode +from collections.abc import Iterator +import json +import os +import socket +import subprocess +import time + +from itsdangerous import TimestampSigner +import httpx +import pytest + + +_SESSION_SECRET = "docker-integration-session-secret" +_WEBHOOK_SECRET = "docker-integration-webhook-secret" +_EMAIL = "integration@example.com" +_APP_ENV = { + "SESSION_SECRET": _SESSION_SECRET, + "WEBHOOK_SECRET": _WEBHOOK_SECRET, + "DISCORD_WEBHOOK_SMOKE": "https://example.com/discord-webhook", + "ALLOWED_EMAILS": _EMAIL, + "KKTIX_ORGANIZATION": "", +} +_WEBHOOK_BODY = { + "notifications": [ + { + "type": "order_activated_paid", + "event": {"slug": "smoke-event", "name": "Smoke Event"}, + "order": {"id": 1001, "paid_at": "2026-08-12T10:00:00+08:00"}, + "contact": { + "name": "Smoke User", + "email": "smoke@example.com", + }, + "tickets": [{"id": 501, "name": "General"}], + } + ] +} + + +@pytest.fixture(scope="module") +def argus_image() -> Iterator[str]: + """Build a clean image and remove it after this module.""" + image = f"argus-integration:{os.getpid()}" + # Always test a clean image from the current files. + _run(["docker", "build", "--no-cache", "--tag", image, "."], timeout=300) + yield image + _run(["docker", "image", "rm", "--force", image], check=False) + + +@pytest.fixture(params=["sqlite", "postgresql"]) +def api_url(request, argus_image: str) -> Iterator[str]: + """Start Argus with each supported database and return its URL.""" + database = request.param + suffix = f"{database}-{os.getpid()}" + network = f"argus-integration-{suffix}" + app = f"argus-app-{suffix}" + db = f"argus-db-{suffix}" + port = _get_free_port() + + _run(["docker", "network", "create", network]) + try: + database_url = "sqlite:////data/argus.db" + if database == "postgresql": + _start_postgresql(db, network) + database_url = "postgresql+psycopg://argus:argus@db:5432/argus" + + _run(_app_command(app, network, port, database_url, argus_image)) + base_url = f"http://127.0.0.1:{port}" + _wait_until_healthy(base_url, app) + yield base_url + finally: + _run(["docker", "rm", "--force", app, db], check=False) + _run(["docker", "network", "rm", network], check=False) + + +def test_docker_image_api_flow(api_url: str) -> None: + """Verify webhook and dashboard APIs through the built image.""" + with httpx.Client( + base_url=api_url, + cookies={"session": _session_cookie(_EMAIL)}, + timeout=5, + ) as client: + for _ in range(2): + webhook = client.post( + "/webhook/kktix/smoke", + headers={"x-kktix-secret": _WEBHOOK_SECRET}, + json=_WEBHOOK_BODY, + ) + assert webhook.status_code == 200 + assert webhook.json() == {"ok": True} + + events = client.get("/dashboard/api/events") + assert events.status_code == 200 + assert events.json()[0]["event_slug"] == "smoke-event" + + timeseries = client.get("/dashboard/api/events/smoke-event/timeseries") + assert timeseries.status_code == 200 + assert timeseries.json()["datasets"] == [ + {"name": "Total", "data": [1]}, + {"name": "General", "data": [1]}, + ] + + logs = client.get("/dashboard/api/webhook-logs") + assert logs.status_code == 200 + assert logs.json()["total"] == 2 + assert "smoke@example.com" not in logs.json()["items"][0]["body"] + + deleted = client.delete("/dashboard/api/events/smoke-event") + assert deleted.status_code == 200 + assert deleted.json() == {"ok": True, "deleted_slug": "smoke-event"} + assert client.get("/dashboard/api/events").json() == [] + + +def _start_postgresql(container: str, network: str) -> None: + _run( + [ + "docker", + "run", + "--detach", + "--name", + container, + "--network", + network, + "--network-alias", + "db", + "--env", + "POSTGRES_DB=argus", + "--env", + "POSTGRES_USER=argus", + "--env", + "POSTGRES_PASSWORD=argus", + "--health-cmd", + "pg_isready -U argus -d argus", + "--health-interval", + "1s", + "--health-timeout", + "5s", + "--health-retries", + "30", + "postgres:17-alpine", + ] + ) + _wait_for_container_health(container) + + +def _app_command( + container: str, + network: str, + port: int, + database_url: str, + image: str, +) -> list[str]: + command = [ + "docker", + "run", + "--detach", + "--name", + container, + "--network", + network, + "--publish", + f"{port}:8000", + "--tmpfs", + "/data", + "--env", + f"DATABASE_URL={database_url}", + ] + for key, value in _APP_ENV.items(): + command.extend(["--env", f"{key}={value}"]) + return [*command, image] + + +def _session_cookie(email: str) -> str: + data = b64encode(json.dumps({"user": {"email": email}}).encode("utf-8")) + return TimestampSigner(_SESSION_SECRET).sign(data).decode("utf-8") + + +def _get_free_port() -> int: + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def _wait_until_healthy(base_url: str, container: str) -> None: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + try: + response = httpx.get(f"{base_url}/health", timeout=1) + if response.status_code == 200: + assert response.json()["checks"]["database"]["ok"] is True + return + except httpx.TransportError: + pass + time.sleep(0.1) + pytest.fail(f"Argus did not become healthy:\n{_container_logs(container)}") + + +def _wait_for_container_health(container: str) -> None: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + result = _run( + ["docker", "inspect", "--format", "{{.State.Health.Status}}", container] + ) + if result.stdout.strip() == "healthy": + return + time.sleep(0.25) + pytest.fail(f"PostgreSQL did not become healthy:\n{_container_logs(container)}") + + +def _container_logs(container: str) -> str: + result = _run(["docker", "logs", container], check=False) + return result.stdout + result.stderr + + +def _run( + command: list[str], + check: bool = True, + timeout: int = 120, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run(command, capture_output=True, text=True, timeout=timeout) + if check and result.returncode != 0: + pytest.fail( + f"command failed: {' '.join(command)}\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + return result + + +"""End-to-end API tests for the clean Docker image.""" From 454879e1e1b1f072a79e04b9174d623d14816cd2 Mon Sep 17 00:00:00 2001 From: chestercheng Date: Sat, 15 Aug 2026 06:06:40 +0800 Subject: [PATCH 2/4] chore: update lockfile for PostgreSQL driver Signed-off-by: chestercheng --- uv.lock | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/uv.lock b/uv.lock index bfb769f..347d59a 100644 --- a/uv.lock +++ b/uv.lock @@ -65,6 +65,7 @@ dependencies = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, + { name = "psycopg", extra = ["binary"] }, { name = "sqlalchemy" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -88,6 +89,7 @@ requires-dist = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, + { name = "psycopg", extras = ["binary"] }, { name = "sqlalchemy" }, { name = "uvicorn", extras = ["standard"] }, ] @@ -907,6 +909,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/82/df3312c0ca083d5b43b352f27d4dd8b1e614bd334473074715d9e0000da4/psycopg_binary-3.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da", size = 4609813, upload-time = "2026-05-01T23:26:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/d74d542458d3e8ac0571d8a88f57ca369999b9a82f4fa528052d0d7d3e4c/psycopg_binary-3.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e", size = 4676799, upload-time = "2026-05-01T23:26:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/09/67/06bab9c60671999f4c6ceff1b334f3ac1f9fc5789eb467c714623ea21de9/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992", size = 5497050, upload-time = "2026-05-01T23:26:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/023433e2b20f970de1e22d29132a95281277646da0b2e2879dd4ee94b8c1/psycopg_binary-3.3.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4", size = 5172428, upload-time = "2026-05-01T23:26:56.708Z" }, + { url = "https://files.pythonhosted.org/packages/08/cd/ae16da8fde228a38b2fe9269bbc13cf89e0186173f2265600f02d6a71e64/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e", size = 6762746, upload-time = "2026-05-01T23:27:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/4f/81/0ba09fa5f5f88779093a2541a8e02489825721f258ab88058b11d68b3eb5/psycopg_binary-3.3.4-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097", size = 5006033, upload-time = "2026-05-01T23:27:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/629136040cc3497adb442a305710b5913f2a754d4630fc3d3717c4c0df65/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95", size = 4534175, upload-time = "2026-05-01T23:27:18.248Z" }, + { url = "https://files.pythonhosted.org/packages/7c/32/1027f843c6dc2d5d51960ee62cc0c2cf755a4c39455aff1371173edbef7d/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839", size = 4224203, upload-time = "2026-05-01T23:27:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/380a724d9093c74adb14d4fce920ea8327838abb61f760b1448586b14a8e/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007", size = 3954509, upload-time = "2026-05-01T23:27:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/db/cd/895893ae575a09c97ccfd5def070d88993d955ef34df45a881fd5ff506d6/psycopg_binary-3.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c", size = 4259551, upload-time = "2026-05-01T23:27:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/2330a20794e37a3ec609ef2fd8522919ec7a4395a1abf979a8e2d1775cd5/psycopg_binary-3.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d", size = 3572054, upload-time = "2026-05-01T23:27:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" }, +] + [[package]] name = "pycparser" version = "3.0" From 0edd38653c44eb28f1a401151833a404e8a8f797 Mon Sep 17 00:00:00 2001 From: chestercheng Date: Sat, 15 Aug 2026 06:12:24 +0800 Subject: [PATCH 3/4] test: make Docker timeseries assertion date-independent Signed-off-by: chestercheng --- tests/test_docker_integration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_docker_integration.py b/tests/test_docker_integration.py index d85c9d3..0708ecd 100644 --- a/tests/test_docker_integration.py +++ b/tests/test_docker_integration.py @@ -95,10 +95,10 @@ def test_docker_image_api_flow(api_url: str) -> None: timeseries = client.get("/dashboard/api/events/smoke-event/timeseries") assert timeseries.status_code == 200 - assert timeseries.json()["datasets"] == [ - {"name": "Total", "data": [1]}, - {"name": "General", "data": [1]}, - ] + datasets = timeseries.json()["datasets"] + assert [dataset["name"] for dataset in datasets] == ["Total", "General"] + assert all(dataset["data"] for dataset in datasets) + assert all(count == 1 for dataset in datasets for count in dataset["data"]) logs = client.get("/dashboard/api/webhook-logs") assert logs.status_code == 200 From fccd24fa8fb85a0b3a15f53009754e825ddf2a5a Mon Sep 17 00:00:00 2001 From: chestercheng Date: Sat, 15 Aug 2026 06:20:30 +0800 Subject: [PATCH 4/4] test: cover PostgreSQL connection timeout Signed-off-by: chestercheng --- tests/test_health.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_health.py b/tests/test_health.py index a4b1411..d3633ce 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -4,7 +4,26 @@ import pytest -from argus import config, health +from argus import config, database, health + + +def test_create_db_engine_rounds_postgresql_connect_timeout(monkeypatch): + """Configure PostgreSQL's integer timeout without opening a connection.""" + captured = {} + + def fake_create_engine(database_url, **kwargs): + captured["database_url"] = database_url + captured.update(kwargs) + return object() + + monkeypatch.setattr(database, "create_engine", fake_create_engine) + + database.create_db_engine("postgresql+psycopg://user:pass@db/argus", 1.2) + + assert captured == { + "database_url": "postgresql+psycopg://user:pass@db/argus", + "connect_args": {"connect_timeout": 2}, + } def test_check_database_succeeds_with_sqlite_url(tmp_path, monkeypatch):