145 lines
5.4 KiB
Python
145 lines
5.4 KiB
Python
import json
|
|
|
|
import pytest
|
|
from django.contrib.auth.models import User
|
|
from django.test import Client
|
|
|
|
from finance.models import Audit, Investigation, Membership
|
|
|
|
|
|
def test_api_auth_method_and_request_shape_guards(workspace):
|
|
ds, _, client, *_ = workspace
|
|
anonymous = Client()
|
|
assert anonymous.get(f"/api/datasets/{ds.pk}").status_code == 401
|
|
assert client.get(f"/api/datasets/{ds.pk}/query").status_code == 405
|
|
assert (
|
|
client.post(f"/api/datasets/{ds.pk}/query", data="[]", content_type="application/json").status_code
|
|
== 400
|
|
)
|
|
assert (
|
|
client.post(f"/api/datasets/{ds.pk}/query", data="{}", content_type="text/plain").status_code == 400
|
|
)
|
|
bad_login = anonymous.post(
|
|
"/api/login", data=json.dumps({"username": "x", "password": "wrong"}), content_type="application/json"
|
|
)
|
|
assert bad_login.status_code == 401
|
|
assert anonymous.post("/api/logout").status_code == 401
|
|
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def test_session_login_logout_and_health_contract(workspace):
|
|
ds, _, _, _, _ = workspace
|
|
client = Client()
|
|
assert client.get("/api/health").json() == {"status": "ok"}
|
|
session = client.get("/api/session").json()
|
|
assert session["user"] is None
|
|
login = client.post(
|
|
"/api/login",
|
|
data=json.dumps({"username": ds.created_by.username, "password": "Finance-TestPassword991!"}),
|
|
content_type="application/json",
|
|
)
|
|
assert login.status_code == 200
|
|
assert client.get("/api/session").json()["user"]["role"] == "admin"
|
|
assert client.post("/api/logout").json() == {"ok": True}
|
|
assert client.get("/api/session").json()["user"] is None
|
|
assert Audit.objects.filter(action__in=["auth.login", "auth.logout"]).count() == 2
|
|
|
|
|
|
def test_user_management_enforces_org_scope_and_self_protection(workspace):
|
|
ds, _, client, _, _ = workspace
|
|
base = {"role": "analyst", "departments": ["儿科"]}
|
|
created = client.post(
|
|
"/api/users",
|
|
data=json.dumps({**base, "username": "managed-user", "password": "Managed-TestPassword991!"}),
|
|
content_type="application/json",
|
|
)
|
|
assert created.status_code == 200
|
|
managed = User.objects.get(username="managed-user")
|
|
membership = Membership.objects.get(user=managed)
|
|
assert membership.organization == ds.organization
|
|
assert membership.departments == ["儿科"]
|
|
|
|
denied = client.post(
|
|
"/api/users",
|
|
data=json.dumps({**base, "username": "weak-user", "password": "123"}),
|
|
content_type="application/json",
|
|
)
|
|
assert denied.status_code == 400
|
|
assert (
|
|
client.patch(
|
|
"/api/users",
|
|
data=json.dumps({"id": ds.created_by_id, "active": False, **base}),
|
|
content_type="application/json",
|
|
).status_code
|
|
== 400
|
|
)
|
|
assert (
|
|
client.patch(
|
|
"/api/users",
|
|
data=json.dumps({"id": managed.pk, "active": False, "role": "viewer", "departments": []}),
|
|
content_type="application/json",
|
|
).status_code
|
|
== 200
|
|
)
|
|
managed.refresh_from_db()
|
|
membership.refresh_from_db()
|
|
assert not managed.is_active
|
|
assert membership.role == "viewer"
|
|
|
|
assert client.get("/api/users").status_code == 200
|
|
assert client.get("/api/audit").status_code == 200
|
|
assert Audit.objects.filter(action="user.update").count() == 2
|
|
|
|
|
|
def test_investigation_lifecycle_and_viewer_write_denial(workspace):
|
|
ds, _, client, _, _ = workspace
|
|
context = {"start": "2026-08", "end": "2026-08", "comparison": "yoy", "group": "department"}
|
|
url = f"/api/datasets/{ds.pk}/investigations"
|
|
saved = client.post(
|
|
url,
|
|
data=json.dumps({**context, "status": "reviewing", "note": "复核收入成本"}),
|
|
content_type="application/json",
|
|
)
|
|
assert saved.status_code == 200
|
|
assert Investigation.objects.filter(dataset=ds, status="reviewing", note="复核收入成本").exists()
|
|
assert client.get(url).json()["items"][0]["status"] == "reviewing"
|
|
invalid = client.post(
|
|
url,
|
|
data=json.dumps({**context, "status": "closed", "note": ""}),
|
|
content_type="application/json",
|
|
)
|
|
assert invalid.status_code == 400
|
|
|
|
viewer = User.objects.create_user("finance-viewer", password="Finance-TestPassword991!")
|
|
Membership.objects.create(user=viewer, organization=ds.organization, role="viewer")
|
|
readonly = Client()
|
|
assert readonly.login(username=viewer.username, password="Finance-TestPassword991!")
|
|
denied = readonly.post(
|
|
url,
|
|
data=json.dumps({**context, "status": "resolved", "note": ""}),
|
|
content_type="application/json",
|
|
)
|
|
assert denied.status_code == 403
|
|
|
|
|
|
def test_evidence_rejects_bad_pagination_and_exports_exact_xlsx(workspace):
|
|
ds, _, client, *_ = workspace
|
|
url = f"/api/datasets/{ds.pk}/evidence"
|
|
invalid = client.post(
|
|
url,
|
|
data=json.dumps({"start": "2026-08", "end": "2026-08", "limit": 201}),
|
|
content_type="application/json",
|
|
)
|
|
assert invalid.status_code == 400
|
|
exported = client.post(
|
|
f"/api/datasets/{ds.pk}/export",
|
|
data=json.dumps({"start": "2026-08", "end": "2026-08", "format": "xlsx", "group": "department"}),
|
|
content_type="application/json",
|
|
)
|
|
assert exported.status_code == 200
|
|
assert exported.streaming
|
|
assert exported["Content-Disposition"].endswith('"traceiq-evidence.xlsx"')
|
|
assert b"".join(exported.streaming_content).startswith(b"PK")
|