Added testing of Profile

This commit is contained in:
StyxX65 2026-04-21 20:51:37 +02:00
parent f7f1194d63
commit 2a2d79de90
5 changed files with 111 additions and 5 deletions

View File

@ -11,7 +11,7 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
### Fixed
- **Profile copy rename not reflected in left column until modal reopen** — saving a renamed profile via the full editor (`_pmgmtSaveFullEdit`) called `loadProfiles()` to refresh `S._profiles` but never called `_renderProfileMgmt()`, so the left-column list was not repainted. The new name only appeared after closing and reopening the modal. Fixed by calling `_renderProfileMgmt()` immediately after `loadProfiles()` and re-applying the `.active` highlight to the correct row.
- **Profile copy rename not reflected in left column until modal reopen** — saving a renamed profile via the full editor (`_pmgmtSaveFullEdit`) called `loadProfiles()` to refresh `S._profiles` but never called `_renderProfileMgmt()`, so the left-column list was not repainted. The new name only appeared after closing and reopening the modal. Fixed by calling `_renderProfileMgmt()` immediately after `loadProfiles()` and re-applying the `.active` highlight to the correct row. 10 new route integration tests added for all profile API endpoints; total test count: 182.
---

View File

@ -42,9 +42,9 @@ python -m pytest tests/ -q
## Tests
172 tests in `tests/`. No integration tests for live M365/Google connections.
182 tests in `tests/`. No integration tests for live M365/Google connections.
**`tests/test_route_integration.py`** — 44 Flask test-client tests covering security-sensitive paths: viewer token CRUD and scope validation, `GET /api/db/flagged` role/user scope enforcement, bulk disposition isolation, viewer PIN (set/verify/rate-limit/change/clear), interface PIN gate (multi-step flows require `session["interface_ok"] = True` after PIN set — the `before_request` hook blocks the same endpoint once a PIN exists), scan lock release on `run_scan()` exception, `GET /api/db/sessions` shape and ordering. Uses a tmp-path `ScanDB` monkeypatched into `routes.database._get_db` — tests never touch the real database. Interface PIN tests manipulate the real `config.json` via `setup_method`/`teardown_method` calling `clear_interface_pin()`.
**`tests/test_route_integration.py`** — 54 Flask test-client tests covering security-sensitive paths: viewer token CRUD and scope validation, `GET /api/db/flagged` role/user scope enforcement, bulk disposition isolation, viewer PIN (set/verify/rate-limit/change/clear), interface PIN gate (multi-step flows require `session["interface_ok"] = True` after PIN set — the `before_request` hook blocks the same endpoint once a PIN exists), scan lock release on `run_scan()` exception, `GET /api/db/sessions` shape and ordering, profile routes CRUD and rename (including the rename-after-copy regression). Uses a tmp-path `ScanDB` monkeypatched into `routes.database._get_db` — tests never touch the real database. Interface PIN tests manipulate the real `config.json` via `setup_method`/`teardown_method` calling `clear_interface_pin()`.
**Local-file scan fixtures** — `tests/fixtures/local_files/` holds 13 documents for manual/UI-level testing of the file scanner. 10 should be flagged; 3 are true negatives. All CPR numbers verified against `is_valid_cpr`. `generate_fixtures.py` (requires `python-docx` + `openpyxl`, already in venv) regenerates the binary `.docx`/`.xlsx` files.

View File

@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
},
{
"path": "."
}
]
}

View File

@ -601,7 +601,7 @@ pip install pytest
pytest tests/
```
**172 tests across 5 modules — all expected to pass.**
**182 tests across 5 modules — all expected to pass.**
| Module | Tests | Covers |
|---|---|---|
@ -609,7 +609,7 @@ pytest tests/
| `tests/test_app_config.py` | 34 | i18n loading, Article 9 keyword detection, config round-trip, admin PIN, profiles CRUD, Fernet encryption |
| `tests/test_checkpoint.py` | 18 | Checkpoint key stability, save/load/clear, wrong-key isolation, delta token round-trip |
| `tests/test_db.py` | 24 | Scan lifecycle, CPR hash-only storage, data subject lookup, dispositions, export/import cycle |
| `tests/test_route_integration.py` | 44 | Viewer token CRUD, role/user scope enforcement, bulk disposition isolation, viewer PIN, interface PIN gate, scan lock release on failure, session history ordering |
| `tests/test_route_integration.py` | 54 | Viewer token CRUD, role/user scope enforcement, bulk disposition isolation, viewer PIN, interface PIN gate, scan lock release on failure, session history ordering, profile routes CRUD and rename |
Each unit-test module (`cpr_detector.py`, `app_config.py`, `checkpoint.py`, `gdpr_db.py`) is importable in isolation without Flask or MSAL — tests run without any cloud credentials or a running server.

View File

@ -9,6 +9,7 @@ Covers:
- Interface PIN set / gate / clear
- Scan lock always released (even when run_scan raises)
- GET /api/db/sessions basic shape
- Profile routes CRUD and rename
"""
from __future__ import annotations
import time
@ -522,3 +523,98 @@ class TestDbSessions:
assert len(sessions) == 2
# Newest session (highest ref_scan_id) must be first
assert sessions[0]["ref_scan_id"] > sessions[1]["ref_scan_id"]
# ---------------------------------------------------------------------------
# Profile routes
# ---------------------------------------------------------------------------
class TestProfileRoutes:
"""
Tests for GET /api/profiles, POST /api/profiles/save,
GET /api/profiles/get, and POST /api/profiles/delete.
Each test monkeypatches the profile storage path to a tmp directory so
tests are fully isolated from the real ~/.gdprscanner/settings.json.
"""
@pytest.fixture(autouse=True)
def _isolate(self, tmp_path, monkeypatch):
import app_config
monkeypatch.setattr(app_config, "_SETTINGS_PATH", tmp_path / "settings.json")
def test_list_returns_empty_list_initially(self, client):
r = client.get("/api/profiles")
assert r.status_code == 200
assert r.get_json()["profiles"] == []
def test_save_missing_name_returns_400(self, client):
r = client.post("/api/profiles/save", json={"sources": ["email"]})
assert r.status_code == 400
assert "error" in r.get_json()
def test_save_creates_profile_and_returns_it(self, client):
r = client.post("/api/profiles/save", json={
"id": "", "name": "Alpha", "sources": ["email"], "options": {}
})
assert r.status_code == 200
data = r.get_json()
assert data["status"] == "saved"
assert data["profile"]["name"] == "Alpha"
assert data["profile"]["id"] # server assigned a non-empty id
def test_saved_profile_appears_in_list(self, client):
client.post("/api/profiles/save", json={"name": "Beta", "sources": [], "options": {}})
profiles = client.get("/api/profiles").get_json()["profiles"]
assert any(p["name"] == "Beta" for p in profiles)
def test_rename_updates_name_in_list(self, client):
"""Regression: _pmgmtSaveFullEdit renames the copy — the API must
persist the new name so loadProfiles() returns fresh data for the
left-column re-render."""
r = client.post("/api/profiles/save", json={
"id": "", "name": "LOCAL-TEST (copy)", "sources": [], "options": {}
})
profile_id = r.get_json()["profile"]["id"]
# Simulate the user renaming the copy in the editor and clicking Save
r2 = client.post("/api/profiles/save", json={
"id": profile_id, "name": "LOCAL-TEST-2", "sources": [], "options": {}
})
assert r2.status_code == 200
assert r2.get_json()["profile"]["name"] == "LOCAL-TEST-2"
profiles = client.get("/api/profiles").get_json()["profiles"]
names = [p["name"] for p in profiles]
assert "LOCAL-TEST-2" in names
assert "LOCAL-TEST (copy)" not in names
def test_get_by_id(self, client):
r = client.post("/api/profiles/save", json={
"id": "fixed-id-1", "name": "Gamma", "sources": [], "options": {}
})
profile_id = r.get_json()["profile"]["id"]
r2 = client.get(f"/api/profiles/get?id={profile_id}")
assert r2.status_code == 200
assert r2.get_json()["profile"]["name"] == "Gamma"
def test_get_nonexistent_returns_404(self, client):
r = client.get("/api/profiles/get?id=does-not-exist")
assert r.status_code == 404
def test_delete_removes_profile(self, client):
client.post("/api/profiles/save", json={"name": "ToDelete", "sources": [], "options": {}})
r = client.post("/api/profiles/delete", json={"name": "ToDelete"})
assert r.status_code == 200
assert r.get_json()["status"] == "deleted"
profiles = client.get("/api/profiles").get_json()["profiles"]
assert not any(p["name"] == "ToDelete" for p in profiles)
def test_delete_nonexistent_returns_not_found(self, client):
r = client.post("/api/profiles/delete", json={"name": "Ghost"})
assert r.status_code == 200
assert r.get_json()["status"] == "not_found"
def test_delete_missing_key_returns_400(self, client):
r = client.post("/api/profiles/delete", json={})
assert r.status_code == 400