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
|
### 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
|
## 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.
|
**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/
|
pytest tests/
|
||||||
```
|
```
|
||||||
|
|
||||||
**172 tests across 5 modules — all expected to pass.**
|
**182 tests across 5 modules — all expected to pass.**
|
||||||
|
|
||||||
| Module | Tests | Covers |
|
| 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_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_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_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.
|
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
|
- Interface PIN set / gate / clear
|
||||||
- Scan lock always released (even when run_scan raises)
|
- Scan lock always released (even when run_scan raises)
|
||||||
- GET /api/db/sessions basic shape
|
- GET /api/db/sessions basic shape
|
||||||
|
- Profile routes CRUD and rename
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import time
|
import time
|
||||||
@ -522,3 +523,98 @@ class TestDbSessions:
|
|||||||
assert len(sessions) == 2
|
assert len(sessions) == 2
|
||||||
# Newest session (highest ref_scan_id) must be first
|
# Newest session (highest ref_scan_id) must be first
|
||||||
assert sessions[0]["ref_scan_id"] > sessions[1]["ref_scan_id"]
|
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