import os import re from conftest import AUTH, create_pin BACKUP_RE = re.compile(r"^pinvault-backup-\d{8}-\d{6}\.db$") def test_bootstrap_ok(client): import app assert app._bootstrap_ok is True def test_backup_ran_with_empty_nas_dir(client): """Regression: bootstrap must not crash when PINVAULT_NAS_BACKUP_DIR is empty.""" import app assert app.NAS_BACKUP_DIR == "" files = os.listdir(app.LOCAL_BACKUP_DIR) assert any(BACKUP_RE.match(f) for f in files) def test_missing_api_key_rejected(client): assert client.get("/api/pins").status_code == 401 assert client.post("/api/pins", json={}).status_code == 401 def test_status(client): resp = client.get("/api/status", headers=AUTH) assert resp.status_code == 200 assert resp.get_json()["locked"] is False def test_list_pins(client): resp = client.get("/api/pins", headers=AUTH) assert resp.status_code == 200 assert isinstance(resp.get_json(), list) def test_create_pin(client): data = create_pin(client, label="home", lock_days=14) assert data["label"] == "home" assert len(data["pin"]) == 4 and data["pin"].isdigit() assert len(data["recovery_codes"]) == 4 assert all(len(c) == 64 for c in data["recovery_codes"]) assert data["id"] > 0 def test_create_pin_lock_days_validation(client): for bad in ("-1", "abc", 1.5, True, None): resp = client.post("/api/pins", json={"lock_days": bad}, headers=AUTH) assert resp.status_code == 400, f"lock_days={bad!r} should be rejected" def test_access_locked_pin_denied(client): data = create_pin(client, lock_days=30) resp = client.post(f"/api/pins/{data['id']}/access", json={}, headers=AUTH) assert resp.status_code == 423 def test_access_unlocked_pin(client): data = create_pin(client, lock_days=0) resp = client.post(f"/api/pins/{data['id']}/access", json={}, headers=AUTH) assert resp.status_code == 200 assert resp.get_json()["pin"] == data["pin"] def test_access_nonexistent_pin(client): resp = client.post("/api/pins/999999/access", json={}, headers=AUTH) assert resp.status_code == 404 def test_recovery_code_unlocks_pin(client): data = create_pin(client, lock_days=30) code = data["recovery_codes"][0] resp = client.post( f"/api/pins/{data['id']}/access", json={"bypass_code": code}, headers=AUTH ) assert resp.status_code == 200 assert resp.get_json()["pin"] == data["pin"] def test_recovery_code_single_use(client): data = create_pin(client, lock_days=30) code = data["recovery_codes"][0] url = f"/api/pins/{data['id']}/access" first = client.post(url, json={"bypass_code": code}, headers=AUTH) assert first.status_code == 200 second = client.post(url, json={"bypass_code": code}, headers=AUTH) assert second.status_code == 403 def test_invalid_recovery_code(client): data = create_pin(client, lock_days=30) resp = client.post( f"/api/pins/{data['id']}/access", json={"bypass_code": "x" * 64}, headers=AUTH ) assert resp.status_code == 403 def test_delete_unrevealed_pin_refused(client): data = create_pin(client, lock_days=30) resp = client.delete(f"/api/pins/{data['id']}", headers=AUTH) assert resp.status_code == 409 def test_delete_revealed_pin(client): data = create_pin(client, lock_days=0) assert client.post(f"/api/pins/{data['id']}/access", json={}, headers=AUTH).status_code == 200 resp = client.delete(f"/api/pins/{data['id']}", headers=AUTH) assert resp.status_code == 200 ids = [p["id"] for p in client.get("/api/pins", headers=AUTH).get_json()] assert data["id"] not in ids def test_backups_listed(client): resp = client.get("/api/backups", headers=AUTH) assert resp.status_code == 200 items = resp.get_json() assert items, "expected at least the bootstrap backup" for item in items: assert BACKUP_RE.match(item["filename"]) assert item["location"] in ("local", "nas") assert item["size"] > 0 def test_trigger_backup(client): resp = client.post("/api/backups", headers=AUTH) assert resp.status_code == 200 assert resp.get_json()["ok"] is True def test_restore_invalid_filename(client): resp = client.post("/api/backups/evil.db/restore", headers=AUTH) assert resp.status_code == 400 def test_restore_missing_backup(client): resp = client.post("/api/backups/pinvault-backup-99999999-999999.db/restore", headers=AUTH) assert resp.status_code == 404 def test_restore_valid_backup(client): filename = client.get("/api/backups", headers=AUTH).get_json()[0]["filename"] resp = client.post(f"/api/backups/{filename}/restore", headers=AUTH) assert resp.status_code == 200 assert resp.get_json()["restored"] == filename def test_list_pins_shape(client): data = create_pin(client, label="shape", lock_days=10) entry = next(p for p in client.get("/api/pins", headers=AUTH).get_json() if p["id"] == data["id"]) for key in ("id", "label", "created_at", "lock_until", "locked", "days_remaining", "revealed", "bypassed_count", "remaining_codes"): assert key in entry, f"missing key {key}" assert entry["locked"] is True assert entry["remaining_codes"] == 4 assert "pin" not in entry and "recovery_codes" not in entry