feat: lock concurrent moderation decisions
This commit is contained in:
+14
-11
@@ -286,8 +286,11 @@ def test_user_report_requires_moderation_before_activity() -> None:
|
||||
assert pending.status_code == 200
|
||||
assert pending.headers["Cache-Control"] == "no-store"
|
||||
assert any(item["id"] == report_id for item in pending.json())
|
||||
approved = client.patch(f"/api/v1/admin/catch-reports/{report_id}", headers=headers, json={"status": "approved", "reason": "fixture verified"})
|
||||
approved = client.patch(f"/api/v1/admin/catch-reports/{report_id}", headers=headers, json={"status": "approved", "reason": "fixture verified", "expected_version": 0})
|
||||
assert approved.status_code == 200
|
||||
stale = client.patch(f"/api/v1/admin/catch-reports/{report_id}", headers=headers, json={"status": "rejected", "reason": "stale tab", "expected_version": 0})
|
||||
assert stale.status_code == 409
|
||||
assert "reload" in stale.json()["detail"]
|
||||
activity = client.get("/api/v1/activity?waterbody=test-lake&fish=pike&hours=24").json()
|
||||
assert any(item["x"] == 77 and item["catches"] == 1 for item in activity["items"])
|
||||
|
||||
@@ -312,19 +315,19 @@ def test_external_observation_requires_mapping_and_complete_data_before_publicat
|
||||
ExternalObservation.source_external_id == "review-complete"
|
||||
))
|
||||
headers = {"Authorization": "Bearer change-me-in-production"}
|
||||
premature = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers)
|
||||
premature = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers, json={"expected_version": 0})
|
||||
assert premature.status_code == 409
|
||||
mapped = client.patch(
|
||||
f"/api/v1/admin/external-observations/{observation_id}/mapping", headers=headers,
|
||||
json={"fish_slug": "pike", "waterbody_slug": "test-lake", "note": "verified fixture"},
|
||||
json={"fish_slug": "pike", "waterbody_slug": "test-lake", "note": "verified fixture", "expected_version": 0},
|
||||
)
|
||||
assert mapped.status_code == 200
|
||||
assert mapped.json()["status"] == "ready"
|
||||
published = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers)
|
||||
published = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers, json={"expected_version": 1})
|
||||
assert published.status_code == 200
|
||||
assert published.json()["status"] == "published"
|
||||
repeated = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers)
|
||||
assert repeated.json()["catch_report_id"] == published.json()["catch_report_id"]
|
||||
repeated = client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers, json={"expected_version": 1})
|
||||
assert repeated.status_code == 409
|
||||
with Session(engine) as db:
|
||||
observation = db.get(ExternalObservation, observation_id)
|
||||
report = db.get(CatchReport, observation.catch_report_id)
|
||||
@@ -365,13 +368,13 @@ def test_incomplete_external_observation_is_publicly_labelled_but_not_counted()
|
||||
assert all(item["x"] != 32 or item["y"] != 42 for item in client.get("/api/v1/activity").json()["items"])
|
||||
mapped = client.patch(
|
||||
f"/api/v1/admin/external-observations/{observation_id}/mapping", headers=headers,
|
||||
json={"fish_slug": "pike", "waterbody_slug": "test-lake"},
|
||||
json={"fish_slug": "pike", "waterbody_slug": "test-lake", "expected_version": 0},
|
||||
)
|
||||
assert mapped.json()["status"] == "mapped"
|
||||
assert client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers).status_code == 409
|
||||
assert client.post(f"/api/v1/admin/external-observations/{observation_id}/publish", headers=headers, json={"expected_version": 1}).status_code == 409
|
||||
rejected = client.patch(
|
||||
f"/api/v1/admin/external-observations/{observation_id}/reject", headers=headers,
|
||||
json={"reason": "weight is absent"},
|
||||
json={"reason": "weight is absent", "expected_version": 1},
|
||||
)
|
||||
assert rejected.json()["status"] == "rejected"
|
||||
assert all(item["id"] != str(observation_id) for item in client.get("/api/v1/community-observations").json())
|
||||
@@ -430,7 +433,7 @@ def test_admin_delete_anonymizes_report_removes_screenshot_and_keeps_audit(monke
|
||||
deleted_keys: list[str] = []
|
||||
monkeypatch.setattr("app.main.delete_screenshot", deleted_keys.append)
|
||||
headers = {"Authorization": "Bearer change-me-in-production"}
|
||||
response = client.delete(f"/api/v1/admin/catch-reports/{created['id']}", headers=headers)
|
||||
response = client.delete(f"/api/v1/admin/catch-reports/{created['id']}?expected_version=0", headers=headers)
|
||||
assert response.status_code == 204
|
||||
assert deleted_keys == ["reports/private.jpg"]
|
||||
with Session(engine) as db:
|
||||
@@ -442,7 +445,7 @@ def test_admin_delete_anonymizes_report_removes_screenshot_and_keeps_audit(monke
|
||||
event = db.query(ModerationEvent).filter_by(catch_report_id=report.id).order_by(ModerationEvent.created_at.desc()).first()
|
||||
assert event is not None
|
||||
assert event.reason == "user report deleted and anonymized"
|
||||
assert client.delete(f"/api/v1/admin/catch-reports/{created['id']}", headers=headers).status_code == 404
|
||||
assert client.delete(f"/api/v1/admin/catch-reports/{created['id']}?expected_version=0", headers=headers).status_code == 404
|
||||
|
||||
|
||||
def test_catch_report_idempotency_key_prevents_duplicates(monkeypatch) -> None:
|
||||
|
||||
Reference in New Issue
Block a user