From bad5351f9d758d5b69531458e4e417739a03f044 Mon Sep 17 00:00:00 2001
From: Frederick Muriuki Muriithi
Date: Fri, 28 Aug 2026 15:10:11 +0000
Subject: test(gn3/case-attr): access-control level 1 & 2 for edit endpoints
Case-attribute names are public; tests cover only write endpoints.
Two parametrized tests over both the flat /case-attribute/ and the
new /api/v1/species//populations/
/case-attributes/ hierarchy:
- test_no_token_edit_returns_400: POST /edit with no token must return 400
- test_no_privilege_edit_returns_401: POST /edit with unprivileged token must return 401
Both tests are expected RED until the can_edit privilege check is added
to the flat and v1 edit endpoints.
Co-Authored-By: Claude Sonnet 4.6
Reviewed-By: Frederick M. Muriithi
---
pyproject.toml | 1 +
tests/test_gn3_case_attr_access.py | 50 ++++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+)
create mode 100644 tests/test_gn3_case_attr_access.py
diff --git a/pyproject.toml b/pyproject.toml
index 773b57a..0a1d58d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ markers = [
"gn_auth: Tests exercising the gn-auth service",
"auth_flow: Tests requiring valid user credentials (set GN_TEST_EMAIL and GN_TEST_PASSWORD)",
"transient: Tests known to fail intermittently due to timing or load; candidates for retry logic",
+ "case_attr_access: Tests for case-attribute endpoint access-control levels",
]
[build-system]
diff --git a/tests/test_gn3_case_attr_access.py b/tests/test_gn3_case_attr_access.py
new file mode 100644
index 0000000..e2399c8
--- /dev/null
+++ b/tests/test_gn3_case_attr_access.py
@@ -0,0 +1,50 @@
+"""Access-control tests for case-attribute endpoints (old flat + v1 hierarchy).
+
+Case-attribute names are publicly accessible (no token required).
+All write endpoints require a valid token, and a token with appropriate
+edit privileges.
+"""
+import pytest
+
+pytestmark = pytest.mark.gn3
+
+_SPECIES_ID = 1 # Mouse
+_INBREDSET_ID = 1 # BXD
+
+
+@pytest.mark.case_attr_access
+@pytest.mark.parametrize("path", [
+ f"/case-attribute/{_INBREDSET_ID}/edit",
+ f"/v1/species/{_SPECIES_ID}/populations/{_INBREDSET_ID}/case-attributes/edit",
+])
+def test_no_token_edit_returns_400(gn3_url, http, path):
+ """POST to any edit endpoint with no token must be rejected with 400."""
+ resp = http.post(
+ f"{gn3_url}{path}",
+ json={"edit-data": []},
+ timeout=30,
+ )
+ assert resp.status_code == 400, (
+ f"Expected 400 for unauthenticated POST {path!r}, "
+ f"got {resp.status_code}. Body: {resp.text[:200]}"
+ )
+
+
+@pytest.mark.case_attr_access
+@pytest.mark.auth_flow
+@pytest.mark.parametrize("path", [
+ f"/case-attribute/{_INBREDSET_ID}/edit",
+ f"/v1/species/{_SPECIES_ID}/populations/{_INBREDSET_ID}/case-attributes/edit",
+])
+def test_no_privilege_edit_returns_401(gn3_url, http, basic_access_token, path):
+ """A token with no case-attribute edit privileges must be refused with 401."""
+ resp = http.post(
+ f"{gn3_url}{path}",
+ json={"edit-data": []},
+ headers={"Authorization": f"Bearer {basic_access_token}"},
+ timeout=30,
+ )
+ assert resp.status_code == 401, (
+ f"Expected 401 for unprivileged POST {path!r}, "
+ f"got {resp.status_code}. Body: {resp.text[:200]}"
+ )
--
cgit 1.4.1