Files
awoooi/apps/api/tests/test_asset_scanner_job.py
ogt b6ddee7984
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m27s
CD Pipeline / build-and-deploy (push) Failing after 3m57s
CD Pipeline / post-deploy-checks (push) Has been skipped
fix(security): reconcile asset control plane truth
2026-07-14 10:57:30 +08:00

340 lines
9.7 KiB
Python

from __future__ import annotations
import asyncio
from src.jobs import asset_scanner_job
class _Result:
rowcount = 1
def scalar(self):
return "00000000-0000-0000-0000-000000000001"
def one(self):
return 1, True
class _Database:
async def execute(self, statement, parameters=None):
return _Result()
class _Context:
async def __aenter__(self):
return _Database()
async def __aexit__(self, exc_type, exc, traceback):
return False
def test_background_scanner_scopes_every_database_operation(monkeypatch) -> None:
requested_projects: list[str | None] = []
def fake_db_context(project_id: str | None = None):
requested_projects.append(project_id)
return _Context()
monkeypatch.setattr("src.db.base.get_db_context", fake_db_context)
async def exercise_scanner_writes() -> None:
run_id = await asset_scanner_job._start_discovery_run(
"cron", ["k8s", "prometheus"], "shallow"
)
assert run_id == "00000000-0000-0000-0000-000000000001"
inserted, modified = await asset_scanner_job._upsert_assets(
[
{
"asset_key": "host/test",
"asset_type": "host",
"host": "test",
"namespace": None,
"name": "test",
"metadata": {},
"tags": ["source:test"],
}
],
run_id,
)
assert (inserted, modified) == (1, 0)
disappeared = await asset_scanner_job._deprecate_missing_assets(
[
{
"asset_key": "host/test",
}
],
("host/",),
run_id,
)
assert disappeared == 1
written = await asset_scanner_job._upsert_relationships(
[
{
"from_key": "host/test",
"to_key": "monitoring/test",
"relationship_type": "monitors",
}
]
)
assert written == 1
await asset_scanner_job._write_coverage_snapshots(run_id)
await asset_scanner_job._finalize_discovery_run(
run_id=run_id,
triggered_by="cron",
status="success",
total_assets=1,
new_assets=1,
modified_assets=0,
disappeared_assets=1,
duration_ms=10,
error=None,
scope=["k8s", "prometheus", "database_catalog"],
scan_depth="shallow",
)
asyncio.run(exercise_scanner_writes())
assert requested_projects == ["awoooi"] * 6
def test_asset_upsert_propagates_database_failure(monkeypatch) -> None:
class FailingDatabase:
async def execute(self, statement, parameters=None):
raise RuntimeError("constraint rejected")
class FailingContext:
async def __aenter__(self):
return FailingDatabase()
async def __aexit__(self, exc_type, exc, traceback):
return False
monkeypatch.setattr(
"src.db.base.get_db_context",
lambda project_id=None: FailingContext(),
)
async def exercise() -> None:
try:
await asset_scanner_job._upsert_assets(
[
{
"asset_key": "postgres/database/awoooi",
"asset_type": "database",
"host": None,
"namespace": None,
"name": "awoooi",
"metadata": {},
"tags": ["source:test"],
}
],
"00000000-0000-0000-0000-000000000001",
)
except RuntimeError as exc:
assert str(exc) == "constraint rejected"
else:
raise AssertionError("asset upsert failure must propagate")
asyncio.run(exercise())
def test_deprecate_missing_assets_is_bounded_to_successful_prefixes(monkeypatch) -> None:
captured: dict[str, object] = {}
class CapturingDatabase:
async def execute(self, statement, parameters=None):
captured["sql"] = str(statement)
captured["parameters"] = parameters
return _Result()
class CapturingContext:
async def __aenter__(self):
return CapturingDatabase()
async def __aexit__(self, exc_type, exc, traceback):
return False
monkeypatch.setattr(
"src.db.base.get_db_context",
lambda project_id=None: CapturingContext(),
)
disappeared = asyncio.run(
asset_scanner_job._deprecate_missing_assets(
[
{"asset_key": "k8s/pod/prod/current"},
{"asset_key": "prometheus_target/node/192.168.0.110:9100"},
],
("k8s/pod/",),
"00000000-0000-0000-0000-000000000001",
)
)
assert disappeared == 1
assert "last_seen_at <" in str(captured["sql"])
assert captured["parameters"] == {
"patterns": ["k8s/pod/%"],
"seen_keys": ["k8s/pod/prod/current"],
"rid": "00000000-0000-0000-0000-000000000001",
}
def test_runtime_collection_reports_failed_sources_without_reconciling_them(
monkeypatch,
) -> None:
async def fake_kubectl(resource: str, all_namespaces: bool = True):
if resource == "nodes":
raise RuntimeError("nodes unavailable")
return {"items": []}
async def fake_prometheus():
return [], []
async def fake_database_catalog():
return [], []
monkeypatch.setattr(asset_scanner_job, "_fetch_kubectl_json", fake_kubectl)
monkeypatch.setattr(
asset_scanner_job,
"_collect_prometheus_targets",
fake_prometheus,
)
monkeypatch.setattr(
asset_scanner_job,
"_collect_database_catalog_assets",
fake_database_catalog,
)
assets, relationships, prefixes, errors = asyncio.run(
asset_scanner_job._collect_runtime_assets()
)
assert assets == []
assert relationships == []
assert "k8s/node/" not in prefixes
assert "k8s/pod/" in prefixes
assert "prometheus_target/" in prefixes
assert errors == ("k8s_nodes",)
def test_scan_terminal_is_failed_when_any_collector_is_incomplete(monkeypatch) -> None:
terminal: dict[str, object] = {}
async def fake_start(*args, **kwargs):
return "00000000-0000-0000-0000-000000000001"
async def fake_collect():
return [], [], (), ("prometheus_targets",)
async def fake_upsert(*args, **kwargs):
return 0, 0
async def fake_deprecate(*args, **kwargs):
return 0
async def fake_relationships(*args, **kwargs):
return 0
async def fake_coverage(*args, **kwargs):
return None
async def fake_finalize(**kwargs):
terminal.update(kwargs)
monkeypatch.setattr(asset_scanner_job, "_start_discovery_run", fake_start)
monkeypatch.setattr(asset_scanner_job, "_collect_runtime_assets", fake_collect)
monkeypatch.setattr(asset_scanner_job, "_upsert_assets", fake_upsert)
monkeypatch.setattr(
asset_scanner_job,
"_deprecate_missing_assets",
fake_deprecate,
)
monkeypatch.setattr(
asset_scanner_job,
"_upsert_relationships",
fake_relationships,
)
monkeypatch.setattr(
asset_scanner_job,
"_write_coverage_snapshots",
fake_coverage,
)
monkeypatch.setattr(
asset_scanner_job,
"_finalize_discovery_run",
fake_finalize,
)
run_id = asyncio.run(asset_scanner_job.scan_once())
assert run_id == "00000000-0000-0000-0000-000000000001"
assert terminal["status"] == "failed"
assert terminal["error"] == (
"RuntimeError: asset_collector_failures=prometheus_targets"
)
def test_database_catalog_collector_reads_metadata_only(monkeypatch) -> None:
requested_projects: list[str | None] = []
class CatalogResult:
def fetchall(self):
return [
type(
"CatalogRow",
(),
{
"_mapping": {
"database_name": "awoooi",
"schema_name": "public",
"relation_name": "incidents",
"relation_kind": "table",
"row_security_enabled": True,
}
},
)()
]
class CatalogDatabase:
async def execute(self, statement, parameters=None):
return CatalogResult()
class CatalogContext:
async def __aenter__(self):
return CatalogDatabase()
async def __aexit__(self, exc_type, exc, traceback):
return False
def fake_db_context(project_id: str | None = None):
requested_projects.append(project_id)
return CatalogContext()
monkeypatch.setattr("src.db.base.get_db_context", fake_db_context)
assets, relationships = asyncio.run(
asset_scanner_job._collect_database_catalog_assets()
)
assert requested_projects == ["awoooi"]
assert [asset["asset_type"] for asset in assets] == ["database", "table"]
assert assets[0]["metadata"]["row_data_read"] is False
assert assets[1]["metadata"] == {
"database": "awoooi",
"schema": "public",
"relation_kind": "table",
"row_security_enabled": True,
"row_data_read": False,
}
assert relationships == [
{
"from_key": "postgres/relation/awoooi/public/incidents",
"to_key": "postgres/database/awoooi",
"relationship_type": "depends_on",
}
]