Skip to content

Commit c984e05

Browse files
Add smoke tests for postgres DBM setup module
Integration tests that run the setup module end-to-end against the live containerized Postgres across the hatch version matrix (9.6 -> 18): dry-run detect/plan, version-specific grant branch, the stdin/stdout CLI contract, and a real apply that verifies idempotency on re-runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e77a575 commit c984e05

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

postgres/tests/test_setup.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
"""Smoke tests for the DBM Postgres setup module.
5+
6+
These run against the live containerized Postgres provided by ``dd_environment``,
7+
which the hatch matrix spins up once per supported server version (9.6 -> 18). The
8+
goal is to prove that ``datadog_checks.postgres.setup`` *executes* correctly across
9+
every version: the Detect/Plan SQL introspection runs without error, version
10+
branching is correct, the stdin/stdout contract the Go agent relies on works, and a
11+
real Apply is idempotent on re-runs.
12+
"""
13+
14+
import json
15+
import subprocess
16+
import sys
17+
18+
import psycopg
19+
import pytest
20+
21+
from datadog_checks.postgres import setup
22+
23+
from .common import DB_NAME, HOST, PASSWORD_ADMIN, PORT, POSTGRES_VERSION, USER_ADMIN
24+
25+
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures('dd_environment')]
26+
27+
SMOKE_USER = 'dd_setup_smoke'
28+
SMOKE_PASSWORD = 'dd_setup_smoke'
29+
30+
ADMIN_URI = f"postgresql://{USER_ADMIN}:{PASSWORD_ADMIN}@{HOST}:{PORT}/{DB_NAME}"
31+
32+
33+
def _admin_connect(dbname=DB_NAME):
34+
"""Autocommit superuser connection for test fixtures/teardown (not via the module)."""
35+
return psycopg.connect(
36+
host=HOST, port=PORT, user=USER_ADMIN, password=PASSWORD_ADMIN, dbname=dbname, autocommit=True
37+
)
38+
39+
40+
def _run(config):
41+
return setup.run_setup({"connection_uri": ADMIN_URI, "config": config})
42+
43+
44+
def _op_types(result):
45+
return {op.get("op_type") for op in result["operations"]}
46+
47+
48+
def _failed_ops(result):
49+
return [op for op in result["operations"] if op.get("status") == "failed"]
50+
51+
52+
def _expected_major():
53+
"""The major version we expect the module to detect, or None when unknown (latest/unpinned)."""
54+
try:
55+
return int(float(POSTGRES_VERSION))
56+
except (TypeError, ValueError):
57+
return None
58+
59+
60+
# ---------------------------------------------------------------------------
61+
# Dry run — the core version smoke test. Exercises all live Detect/Plan SQL
62+
# against every server version with zero mutation.
63+
# ---------------------------------------------------------------------------
64+
65+
66+
def test_setup_dry_run_across_versions():
67+
result = _run(
68+
{
69+
"datadog_user": "datadog", # pre-created by the test fixtures, so no password needed
70+
"databases": [DB_NAME],
71+
"dry_run": True,
72+
}
73+
)
74+
75+
assert result["outcome"] == "dry_run"
76+
assert result["flavor"] == "self_hosted"
77+
assert not _failed_ops(result)
78+
79+
major = result["pg_version"]
80+
assert isinstance(major, int) and major >= 9
81+
expected = _expected_major()
82+
if expected is not None:
83+
assert major == expected
84+
85+
# Version-specific grant branch: pg_monitor exists on 10+, table grants on 9.6.
86+
op_types = _op_types(result)
87+
if major >= 10:
88+
assert "grant_pg_monitor" in op_types
89+
assert "grant_pg96" not in op_types
90+
else:
91+
assert "grant_pg96" in op_types
92+
assert "grant_pg_monitor" not in op_types
93+
94+
# Per-database objects are always planned for the requested database.
95+
for expected_op in ("create_extension", "create_schema", "func_explain_statement"):
96+
assert expected_op in op_types
97+
98+
99+
def test_setup_rejects_read_replica_detection_path():
100+
"""Detect runs pg_is_in_recovery() on every version; on a primary it must not raise."""
101+
result = _run({"datadog_user": "datadog", "databases": [DB_NAME], "dry_run": True})
102+
# Reaching a dry_run result at all proves the primary/standby probe executed and passed.
103+
assert result["outcome"] == "dry_run"
104+
105+
106+
# ---------------------------------------------------------------------------
107+
# stdin/stdout contract — the actual interface the Go agent tunnels into.
108+
# ---------------------------------------------------------------------------
109+
110+
111+
def test_setup_cli_stdin_stdout_contract():
112+
payload = json.dumps(
113+
{
114+
"connection_uri": ADMIN_URI,
115+
"config": {"datadog_user": "datadog", "databases": [DB_NAME], "dry_run": True},
116+
}
117+
)
118+
proc = subprocess.run(
119+
[sys.executable, "-m", "datadog_checks.postgres.setup"],
120+
input=payload,
121+
capture_output=True,
122+
text=True,
123+
)
124+
assert proc.returncode == 0, proc.stderr
125+
envelope = json.loads(proc.stdout)
126+
assert envelope["success"] is True
127+
assert envelope["result"]["outcome"] == "dry_run"
128+
129+
130+
def test_setup_cli_reports_failure_nonzero():
131+
"""A bad connection must surface as success=False and a non-zero exit code."""
132+
payload = json.dumps(
133+
{
134+
"connection_uri": f"postgresql://{USER_ADMIN}:wrong-password@{HOST}:{PORT}/{DB_NAME}",
135+
"config": {"datadog_user": "datadog", "databases": [DB_NAME], "dry_run": True},
136+
}
137+
)
138+
proc = subprocess.run(
139+
[sys.executable, "-m", "datadog_checks.postgres.setup"],
140+
input=payload,
141+
capture_output=True,
142+
text=True,
143+
)
144+
assert proc.returncode == 1
145+
envelope = json.loads(proc.stdout)
146+
assert envelope["success"] is False
147+
assert envelope["error"]
148+
149+
150+
# ---------------------------------------------------------------------------
151+
# Real apply + idempotency. Creates a throwaway user and real DB objects, then
152+
# re-runs to prove the second pass is a clean no-op (the RDS/Aurora re-run fix).
153+
# ---------------------------------------------------------------------------
154+
155+
156+
@pytest.fixture
157+
def smoke_user_cleanup():
158+
yield
159+
with _admin_connect() as conn:
160+
with conn.cursor() as cur:
161+
cur.execute("SELECT 1 FROM pg_roles WHERE rolname = %s", (SMOKE_USER,))
162+
if cur.fetchone():
163+
# Drop privileges the user was granted (schema usage, etc.) before dropping the role.
164+
cur.execute(f'DROP OWNED BY "{SMOKE_USER}"')
165+
cur.execute(f'DROP ROLE "{SMOKE_USER}"')
166+
# Restore the only server-global settings a self-hosted apply can touch here.
167+
with _admin_connect() as conn:
168+
with conn.cursor() as cur:
169+
cur.execute("ALTER SYSTEM RESET track_activity_query_size")
170+
cur.execute('ALTER SYSTEM RESET "pg_stat_statements.track_utility"')
171+
cur.execute("SELECT pg_reload_conf()")
172+
173+
174+
def test_setup_apply_and_idempotency(smoke_user_cleanup):
175+
config = {
176+
"datadog_user": SMOKE_USER,
177+
"datadog_password": SMOKE_PASSWORD,
178+
"databases": [DB_NAME],
179+
}
180+
181+
first = _run(config)
182+
assert first["outcome"] in ("success", "success_with_manual_steps")
183+
assert not _failed_ops(first), _failed_ops(first)
184+
185+
# The user did not exist, so it must have been created and granted monitoring access.
186+
create_user = next(op for op in first["operations"] if op.get("op_type") == "create_user")
187+
assert create_user["status"] == "completed"
188+
# Password must be redacted from the recorded operation.
189+
assert create_user.get("redact") is True
190+
191+
# The role really exists and can authenticate now.
192+
with psycopg.connect(host=HOST, port=PORT, user=SMOKE_USER, password=SMOKE_PASSWORD, dbname=DB_NAME) as conn:
193+
assert conn.info.status is not None
194+
195+
# Second run: nothing should fail, and the user is now detected as existing (no re-create).
196+
second = _run(config)
197+
assert second["outcome"] in ("success", "success_with_manual_steps")
198+
assert not _failed_ops(second), _failed_ops(second)
199+
assert "create_user" not in _op_types(second)

0 commit comments

Comments
 (0)