Added testing of Profile
This commit is contained in:
parent
f7f1194d63
commit
2a2d79de90
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
10
GDPRScanner.code-workspace
Normal file
10
GDPRScanner.code-workspace
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user