Skip to content

Commit 5bc48e3

Browse files
tmdeveloper007utksh1
authored andcommitted
test: add unit tests for _gather_scan_summary in notification_service
1 parent 5267f90 commit 5bc48e3

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
Unit tests for _gather_scan_summary in backend/secuscan/notification_service.py.
3+
4+
The function collects task status, severity counts, and a report link for scan
5+
completion webhooks. It is exercised indirectly through process_scan_completion_webhook
6+
but not directly unit tested.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import tempfile
12+
import uuid
13+
14+
import pytest
15+
import pytest_asyncio
16+
17+
from backend.secuscan import database as database_module
18+
from backend.secuscan.config import settings
19+
from backend.secuscan.database import init_db
20+
from backend.secuscan.notification_service import _gather_scan_summary
21+
22+
23+
@pytest.fixture
24+
def setup_test_environment(monkeypatch):
25+
"""Override settings for tests to ensure isolated execution."""
26+
temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
27+
temp_path = temp_dir.name
28+
29+
monkeypatch.setattr(settings, "data_dir", temp_path)
30+
monkeypatch.setattr(settings, "raw_output_dir", f"{temp_path}/raw")
31+
monkeypatch.setattr(settings, "reports_dir", f"{temp_path}/reports")
32+
monkeypatch.setattr(settings, "database_path", f"{temp_path}/test_secuscan.db")
33+
monkeypatch.setattr(settings, "vault_key", "test-vault-key-for-unit-tests-only")
34+
monkeypatch.setattr(settings, "admin_api_key", "test-admin-key")
35+
monkeypatch.setattr(settings, "enforce_network_policy", False)
36+
monkeypatch.setattr(settings, "scan_rate_limit", 0)
37+
38+
settings.ensure_directories()
39+
40+
yield temp_path
41+
42+
temp_dir.cleanup()
43+
44+
45+
@pytest_asyncio.fixture
46+
async def test_db(setup_test_environment):
47+
db = await init_db(settings.database_path)
48+
yield db
49+
if database_module.db is not None:
50+
await database_module.db.disconnect()
51+
database_module.db = None
52+
53+
54+
async def _seed_task_with_findings(
55+
db,
56+
*,
57+
status: str = "completed",
58+
findings: list[str] | None = None,
59+
tool_name: str = "nmap",
60+
plugin_id: str = "nmap",
61+
owner_id: str = "default",
62+
error_message: str | None = None,
63+
) -> tuple[str, list[str]]:
64+
"""Seed a task and optionally seed findings with given severities."""
65+
task_id = str(uuid.uuid4())
66+
await db.execute(
67+
"""
68+
INSERT INTO tasks (
69+
id, plugin_id, tool_name, target, status, inputs_json, consent_granted, owner_id
70+
) VALUES (?, ?, ?, ?, ?, '{}', 1, ?)
71+
""",
72+
(task_id, plugin_id, tool_name, "https://example.com", status, owner_id),
73+
)
74+
if error_message:
75+
await db.execute(
76+
"UPDATE tasks SET error_message = ? WHERE id = ?",
77+
(error_message, task_id),
78+
)
79+
80+
finding_ids = []
81+
severities = findings or []
82+
for severity in severities:
83+
finding_id = str(uuid.uuid4())
84+
finding_ids.append(finding_id)
85+
await db.execute(
86+
"""
87+
INSERT INTO findings (
88+
id, task_id, plugin_id, title, category, severity, target, description, remediation
89+
) VALUES (?, ?, ?, 'Open port', 'network', ?, 'https://example.com', 'desc', 'fix')
90+
""",
91+
(finding_id, task_id, plugin_id, severity),
92+
)
93+
return task_id, finding_ids
94+
95+
96+
@pytest.mark.asyncio
97+
async def test_returns_none_for_nonexistent_task(test_db):
98+
"""_gather_scan_summary returns None when the task does not exist."""
99+
result = await _gather_scan_summary(test_db, str(uuid.uuid4()))
100+
assert result is None
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_returns_correct_summary_dict(test_db):
105+
"""_gather_scan_summary returns a dict with expected keys."""
106+
task_id, _ = await _seed_task_with_findings(test_db)
107+
result = await _gather_scan_summary(test_db, task_id)
108+
assert result is not None
109+
assert "task_id" in result
110+
assert "tool_name" in result
111+
assert "target" in result
112+
assert "status" in result
113+
assert "total_findings" in result
114+
assert "severity_counts" in result
115+
assert "error_message" in result
116+
assert "report_link" in result
117+
118+
119+
@pytest.mark.asyncio
120+
async def test_severity_counts_are_correct(test_db):
121+
"""Severity counts are correctly aggregated."""
122+
task_id, _ = await _seed_task_with_findings(
123+
test_db,
124+
findings=["critical", "critical", "high", "low"],
125+
)
126+
result = await _gather_scan_summary(test_db, task_id)
127+
assert result["total_findings"] == 4
128+
assert result["severity_counts"]["critical"] == 2
129+
assert result["severity_counts"]["high"] == 1
130+
assert result["severity_counts"]["low"] == 1
131+
132+
133+
@pytest.mark.asyncio
134+
async def test_zero_findings_gives_zero_counts(test_db):
135+
"""A completed task with no findings has total_findings=0 and empty counts."""
136+
task_id, _ = await _seed_task_with_findings(test_db)
137+
result = await _gather_scan_summary(test_db, task_id)
138+
assert result["total_findings"] == 0
139+
assert result["severity_counts"] == {}
140+
141+
142+
@pytest.mark.asyncio
143+
async def test_error_message_included_in_summary(test_db):
144+
"""error_message from the task is included in the summary."""
145+
task_id, _ = await _seed_task_with_findings(
146+
test_db,
147+
status="failed",
148+
error_message="Connection refused",
149+
)
150+
result = await _gather_scan_summary(test_db, task_id)
151+
assert result["error_message"] == "Connection refused"
152+
153+
154+
@pytest.mark.asyncio
155+
async def test_report_link_contains_task_id(test_db):
156+
"""The report link contains the task_id."""
157+
task_id, _ = await _seed_task_with_findings(test_db)
158+
result = await _gather_scan_summary(test_db, task_id)
159+
assert task_id in result["report_link"]
160+
161+
162+
@pytest.mark.asyncio
163+
async def test_status_lowercased(test_db):
164+
"""The status in the summary is lowercased."""
165+
task_id, _ = await _seed_task_with_findings(test_db, status="COMPLETED")
166+
result = await _gather_scan_summary(test_db, task_id)
167+
assert result["status"] == "completed"
168+
169+
170+
@pytest.mark.asyncio
171+
async def test_tool_name_uses_database_value(test_db):
172+
"""tool_name in the summary matches the database value."""
173+
task_id, _ = await _seed_task_with_findings(test_db, tool_name="sqlmap")
174+
result = await _gather_scan_summary(test_db, task_id)
175+
assert result["tool_name"] == "sqlmap"

0 commit comments

Comments
 (0)