test(09-01): add failing tests for retention cleanup service
- Test cleanup deletes expired snapshots - Test snapshots within retention window are kept - Test deleted count is returned - Test empty table handled gracefully Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@
|
|||||||
- [x] **UI-01**: Device page shows Configuration History section below Remote Access
|
- [x] **UI-01**: Device page shows Configuration History section below Remote Access
|
||||||
- [x] **UI-02**: Timeline displays change entries with component, summary, and timestamp
|
- [x] **UI-02**: Timeline displays change entries with component, summary, and timestamp
|
||||||
- [x] **UI-03**: Diff viewer shows unified diff with add/remove highlighting
|
- [x] **UI-03**: Diff viewer shows unified diff with add/remove highlighting
|
||||||
- [ ] **UI-04**: User can download snapshot as `router-{device_name}-{timestamp}.rsc`
|
- [x] **UI-04**: User can download snapshot as `router-{device_name}-{timestamp}.rsc`
|
||||||
|
|
||||||
### Observability
|
### Observability
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
| UI-01 | Phase 7: Config History UI | Complete |
|
| UI-01 | Phase 7: Config History UI | Complete |
|
||||||
| UI-02 | Phase 7: Config History UI | Complete |
|
| UI-02 | Phase 7: Config History UI | Complete |
|
||||||
| UI-03 | Phase 8: Diff Viewer & Download | Complete |
|
| UI-03 | Phase 8: Diff Viewer & Download | Complete |
|
||||||
| UI-04 | Phase 8: Diff Viewer & Download | Pending |
|
| UI-04 | Phase 8: Diff Viewer & Download | Complete |
|
||||||
| OBS-01 | Phase 10: Audit & Observability | Pending |
|
| OBS-01 | Phase 10: Audit & Observability | Pending |
|
||||||
| OBS-02 | Phase 10: Audit & Observability | Pending |
|
| OBS-02 | Phase 10: Audit & Observability | Pending |
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ gsd_state_version: 1.0
|
|||||||
milestone: v9.6
|
milestone: v9.6
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: completed
|
status: completed
|
||||||
stopped_at: Completed 08-01-PLAN.md
|
stopped_at: Completed 08-02-PLAN.md
|
||||||
last_updated: "2026-03-13T04:21:58.035Z"
|
last_updated: "2026-03-13T04:24:44.396Z"
|
||||||
last_activity: 2026-03-13 -- Completed 07-01 config history UI timeline component
|
last_activity: 2026-03-13 -- Completed 08-02 snapshot download
|
||||||
progress:
|
progress:
|
||||||
total_phases: 10
|
total_phases: 10
|
||||||
completed_phases: 7
|
completed_phases: 8
|
||||||
total_plans: 12
|
total_plans: 12
|
||||||
completed_plans: 11
|
completed_plans: 12
|
||||||
percent: 100
|
percent: 92
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -25,12 +25,12 @@ See: .planning/PROJECT.md (updated 2026-03-12)
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 8 of 10 (Diff Viewer & Download)
|
Phase: 8 of 10 (Diff Viewer & Download) -- COMPLETE
|
||||||
Plan: 1 of 2 in current phase
|
Plan: 2 of 2 in current phase
|
||||||
Status: Plan 08-01 complete
|
Status: Phase 08 complete
|
||||||
Last activity: 2026-03-13 -- Completed 08-01 diff viewer component
|
Last activity: 2026-03-13 -- Completed 08-02 snapshot download
|
||||||
|
|
||||||
Progress: [█████████░] 92%
|
Progress: [██████████] 100%
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ Progress: [█████████░] 92%
|
|||||||
| Phase 06 P02 | 2min | 2 tasks | 3 files |
|
| Phase 06 P02 | 2min | 2 tasks | 3 files |
|
||||||
| Phase 07 P01 | 3min | 2 tasks | 3 files |
|
| Phase 07 P01 | 3min | 2 tasks | 3 files |
|
||||||
| Phase 08 P01 | 1min | 2 tasks | 3 files |
|
| Phase 08 P01 | 1min | 2 tasks | 3 files |
|
||||||
|
| Phase 08 P02 | 1min | 1 tasks | 3 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ Recent decisions affecting current work:
|
|||||||
- [Phase 07]: 60s refetchInterval polling for near-real-time config change visibility
|
- [Phase 07]: 60s refetchInterval polling for near-real-time config change visibility
|
||||||
- [Phase 08]: DiffViewer rendered inline above timeline (not modal) for context preservation
|
- [Phase 08]: DiffViewer rendered inline above timeline (not modal) for context preservation
|
||||||
- [Phase 08]: Line classification function for unified diff: +green, -red, @@blue, ---/+++ muted
|
- [Phase 08]: Line classification function for unified diff: +green, -red, @@blue, ---/+++ muted
|
||||||
|
- [Phase 08]: Blob URL download pattern consistent with existing exportMyData and auditLogsApi.exportCsv patterns
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -103,6 +105,6 @@ None yet.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-13T04:21:58.032Z
|
Last session: 2026-03-13T04:24:44.393Z
|
||||||
Stopped at: Completed 08-01-PLAN.md
|
Stopped at: Completed 08-02-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
128
backend/tests/test_retention_service.py
Normal file
128
backend/tests/test_retention_service.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""Tests for retention cleanup service.
|
||||||
|
|
||||||
|
Tests the cleanup_expired_snapshots function with mocked AdminAsyncSessionLocal
|
||||||
|
and mocked settings.CONFIG_RETENTION_DAYS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cleanup_deletes_expired_snapshots():
|
||||||
|
"""Test 1: cleanup_expired_snapshots deletes snapshots with collected_at older than retention_days."""
|
||||||
|
from app.services.retention_service import cleanup_expired_snapshots
|
||||||
|
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.rowcount = 5
|
||||||
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||||
|
mock_session.commit = AsyncMock()
|
||||||
|
|
||||||
|
mock_ctx = AsyncMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.services.retention_service.AdminAsyncSessionLocal",
|
||||||
|
return_value=mock_ctx,
|
||||||
|
), patch(
|
||||||
|
"app.services.retention_service.settings",
|
||||||
|
) as mock_settings:
|
||||||
|
mock_settings.CONFIG_RETENTION_DAYS = 90
|
||||||
|
count = await cleanup_expired_snapshots()
|
||||||
|
|
||||||
|
# Should execute the DELETE query
|
||||||
|
mock_session.execute.assert_called_once()
|
||||||
|
# Verify DELETE uses make_interval with the configured days
|
||||||
|
sql_text = str(mock_session.execute.call_args[0][0].text)
|
||||||
|
assert "make_interval" in sql_text
|
||||||
|
assert "DELETE" in sql_text
|
||||||
|
assert "router_config_snapshots" in sql_text
|
||||||
|
# Should commit
|
||||||
|
mock_session.commit.assert_called_once()
|
||||||
|
# Should return the deleted count
|
||||||
|
assert count == 5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cleanup_keeps_snapshots_within_retention_window():
|
||||||
|
"""Test 2: cleanup_expired_snapshots keeps snapshots within the retention window."""
|
||||||
|
from app.services.retention_service import cleanup_expired_snapshots
|
||||||
|
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.rowcount = 0 # No rows deleted means all within window
|
||||||
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||||
|
mock_session.commit = AsyncMock()
|
||||||
|
|
||||||
|
mock_ctx = AsyncMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.services.retention_service.AdminAsyncSessionLocal",
|
||||||
|
return_value=mock_ctx,
|
||||||
|
), patch(
|
||||||
|
"app.services.retention_service.settings",
|
||||||
|
) as mock_settings:
|
||||||
|
mock_settings.CONFIG_RETENTION_DAYS = 90
|
||||||
|
count = await cleanup_expired_snapshots()
|
||||||
|
|
||||||
|
assert count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cleanup_returns_deleted_count():
|
||||||
|
"""Test 3: cleanup_expired_snapshots returns count of deleted rows."""
|
||||||
|
from app.services.retention_service import cleanup_expired_snapshots
|
||||||
|
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.rowcount = 42
|
||||||
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||||
|
mock_session.commit = AsyncMock()
|
||||||
|
|
||||||
|
mock_ctx = AsyncMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.services.retention_service.AdminAsyncSessionLocal",
|
||||||
|
return_value=mock_ctx,
|
||||||
|
), patch(
|
||||||
|
"app.services.retention_service.settings",
|
||||||
|
) as mock_settings:
|
||||||
|
mock_settings.CONFIG_RETENTION_DAYS = 30
|
||||||
|
count = await cleanup_expired_snapshots()
|
||||||
|
|
||||||
|
assert count == 42
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cleanup_handles_empty_table():
|
||||||
|
"""Test 4: cleanup_expired_snapshots handles empty table (returns 0)."""
|
||||||
|
from app.services.retention_service import cleanup_expired_snapshots
|
||||||
|
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_result = MagicMock()
|
||||||
|
mock_result.rowcount = 0
|
||||||
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
||||||
|
mock_session.commit = AsyncMock()
|
||||||
|
|
||||||
|
mock_ctx = AsyncMock()
|
||||||
|
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.services.retention_service.AdminAsyncSessionLocal",
|
||||||
|
return_value=mock_ctx,
|
||||||
|
), patch(
|
||||||
|
"app.services.retention_service.settings",
|
||||||
|
) as mock_settings:
|
||||||
|
mock_settings.CONFIG_RETENTION_DAYS = 90
|
||||||
|
count = await cleanup_expired_snapshots()
|
||||||
|
|
||||||
|
assert count == 0
|
||||||
|
mock_session.execute.assert_called_once()
|
||||||
|
mock_session.commit.assert_called_once()
|
||||||
Reference in New Issue
Block a user