From 88d490959a3a3d408aa66524f6a4c45c214fb7ad Mon Sep 17 00:00:00 2001 From: dinesh Date: Mon, 2 Feb 2026 16:05:43 -0800 Subject: [PATCH 1/2] demo env setup related changes --- .../submit-api/templates/demo-db-secret.yaml | 14 ++++++++ .../submit-api/templates/deployment.yaml | 36 +++++++++++++++++++ .../charts/submit-api/templates/secret.yaml | 2 +- deployment/charts/submit-api/values.yaml | 6 ++++ .../versions/fb95dbfcb9d9_add_new_status.py | 17 +++++++-- 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 deployment/charts/submit-api/templates/demo-db-secret.yaml diff --git a/deployment/charts/submit-api/templates/demo-db-secret.yaml b/deployment/charts/submit-api/templates/demo-db-secret.yaml new file mode 100644 index 000000000..5d4992a60 --- /dev/null +++ b/deployment/charts/submit-api/templates/demo-db-secret.yaml @@ -0,0 +1,14 @@ +{{- if .Values.database.demo.enabled }} +apiVersion: v1 +kind: Secret +metadata: + labels: + app: {{ .Release.Name }} + name: {{ .Release.Name }}-{{ .Values.database.demo.suffix }} + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} +type: Opaque +stringData: + app-db-username: {{ .Values.database.demo.username | quote }} + app-db-password: {{ .Values.database.demo.password | quote }} + app-db-name: {{ .Values.database.demo.name | quote }} +{{- end }} diff --git a/deployment/charts/submit-api/templates/deployment.yaml b/deployment/charts/submit-api/templates/deployment.yaml index d7a11d8ac..60f6ed92d 100644 --- a/deployment/charts/submit-api/templates/deployment.yaml +++ b/deployment/charts/submit-api/templates/deployment.yaml @@ -25,6 +25,23 @@ spec: command: - /opt/app-root/pre-hook-update-db.sh env: + {{- if .Values.database.demo.enabled }} + - name: DATABASE_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-username + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-password + - name: DATABASE_NAME + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-name + {{- else }} - name: DATABASE_USERNAME valueFrom: secretKeyRef: @@ -40,6 +57,7 @@ spec: secretKeyRef: name: {{ .Values.database.secret }} key: app-db-name + {{- end }} - name: DATABASE_HOST value: {{ .Values.database.service.name }} - name: DATABASE_PORT @@ -52,6 +70,23 @@ spec: - containerPort: 8080 protocol: TCP env: + {{- if .Values.database.demo.enabled }} + - name: DATABASE_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-username + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-password + - name: DATABASE_NAME + valueFrom: + secretKeyRef: + name: {{ .Values.database.secret }}-{{ .Values.database.demo.suffix }} + key: app-db-name + {{- else }} - name: DATABASE_USERNAME valueFrom: secretKeyRef: @@ -67,6 +102,7 @@ spec: secretKeyRef: name: {{ .Values.database.secret }} key: app-db-name + {{- end }} - name: DATABASE_HOST value: {{ .Values.database.service.name }} - name: DATABASE_PORT diff --git a/deployment/charts/submit-api/templates/secret.yaml b/deployment/charts/submit-api/templates/secret.yaml index cef14ea84..ca3737c9b 100644 --- a/deployment/charts/submit-api/templates/secret.yaml +++ b/deployment/charts/submit-api/templates/secret.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: labels: - app: {{ .Values.app_group }} + app: {{ .Release.Name }} name: {{ .Release.Name }} name: {{ .Release.Name }} stringData: diff --git a/deployment/charts/submit-api/values.yaml b/deployment/charts/submit-api/values.yaml index 13df662af..f2df987f1 100644 --- a/deployment/charts/submit-api/values.yaml +++ b/deployment/charts/submit-api/values.yaml @@ -19,6 +19,12 @@ database: service: name: submit-patroni port: 5432 + demo: + enabled: false + suffix: demo + username: "" + password: "" + name: "" service: type: ClusterIP diff --git a/submit-api/migrations/versions/fb95dbfcb9d9_add_new_status.py b/submit-api/migrations/versions/fb95dbfcb9d9_add_new_status.py index 169733936..a78db92cc 100644 --- a/submit-api/migrations/versions/fb95dbfcb9d9_add_new_status.py +++ b/submit-api/migrations/versions/fb95dbfcb9d9_add_new_status.py @@ -22,7 +22,13 @@ def upgrade(): DECLARE itemstatus_values text[]; packagestatus_values text[]; + items_count integer; + packages_count integer; BEGIN + -- Check if tables have data + SELECT COUNT(*) INTO items_count FROM items; + SELECT COUNT(*) INTO packages_count FROM packages; + -- Get existing values SELECT array_agg(quote_literal(enumlabel)) INTO itemstatus_values FROM pg_enum WHERE enumtypid = 'itemstatus'::regtype; SELECT array_agg(quote_literal(enumlabel)) INTO packagestatus_values FROM pg_enum WHERE enumtypid = 'packagestatus'::regtype; @@ -35,9 +41,14 @@ def upgrade(): ALTER TABLE items ALTER COLUMN status TYPE itemstatus_new USING status::text::itemstatus_new; ALTER TABLE packages ALTER COLUMN status TYPE packagestatus_new[] USING status::text[]::packagestatus_new[]; - -- Update data - UPDATE items SET status = 'NEW' WHERE status = 'NEW_SUBMISSION'; - UPDATE packages SET status = array_replace(status, 'NEW_SUBMISSION', 'NEW'); + -- Update data only if tables have rows + IF items_count > 0 THEN + UPDATE items SET status = 'NEW' WHERE status = 'NEW_SUBMISSION'; + END IF; + + IF packages_count > 0 THEN + UPDATE packages SET status = array_replace(status, 'NEW_SUBMISSION', 'NEW'); + END IF; -- Drop old types and rename new ones DROP TYPE itemstatus; From d546f444761a55a0c6dd26947e28a361fa8ba958 Mon Sep 17 00:00:00 2001 From: dinesh Date: Fri, 6 Feb 2026 11:37:06 -0800 Subject: [PATCH 2/2] staff-user endpoint label change plus test cases --- .../submit_api/resources/staff/staff_user.py | 2 +- .../tests/unit/resources/test_staff_user.py | 245 ++++++++++++++++++ submit-web/src/hooks/api/constants.ts | 2 +- submit-web/src/hooks/api/useStaffUser.ts | 4 +- 4 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 submit-api/tests/unit/resources/test_staff_user.py diff --git a/submit-api/src/submit_api/resources/staff/staff_user.py b/submit-api/src/submit_api/resources/staff/staff_user.py index b9a3a1774..cc0cd14d8 100644 --- a/submit-api/src/submit_api/resources/staff/staff_user.py +++ b/submit-api/src/submit_api/resources/staff/staff_user.py @@ -27,7 +27,7 @@ from submit_api.utils.roles import EpicSubmitRole from submit_api.utils.util import allowedorigins, cors_preflight -API = Namespace("staff-user", description="Endpoints for Staff Management") +API = Namespace("staff-users", description="Endpoints for Staff Management") """Custom exception messages """ diff --git a/submit-api/tests/unit/resources/test_staff_user.py b/submit-api/tests/unit/resources/test_staff_user.py new file mode 100644 index 000000000..4093a7645 --- /dev/null +++ b/submit-api/tests/unit/resources/test_staff_user.py @@ -0,0 +1,245 @@ +"""Test Staff User API endpoints. + +Tests for staff user resource endpoints. +""" + +from http import HTTPStatus +from unittest.mock import patch + +from faker import Faker + +from submit_api.models.user import UserType +from tests.utilities.factory_scenarios import TestJwtClaims +from tests.utilities.factory_utils import ( + factory_auth_header, + factory_user_model, +) + +fake = Faker() + + +def test_get_staff_user_by_guid(client, session, jwt): + """Test fetching a staff user by GUID.""" + # Create a staff user + auth_guid = TestJwtClaims.staff_admin_role['preferred_username'] + user = factory_user_model(auth_guid=auth_guid, user_type=UserType.STAFF, session=session) + + # Create staff_user record + from submit_api.models.staff_user import StaffUser + staff_user_data = { + 'first_name': fake.first_name(), + 'last_name': fake.last_name(), + 'work_email_address': fake.email(), + 'user_id': user.id + } + staff_user = StaffUser.create_staff_user(staff_user_data, session=session) + session.commit() + + # Make request + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.get(f"/api/staff/staff-users/{auth_guid}", headers=headers) + + assert response.status_code == HTTPStatus.OK + data = response.get_json() + assert data["id"] == staff_user.id + assert data["first_name"] == staff_user_data['first_name'] + assert data["last_name"] == staff_user_data['last_name'] + assert data["work_email_address"] == staff_user_data['work_email_address'] + + +def test_get_staff_user_not_found(client, session, jwt): + """Test fetching a non-existent staff user.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.get("/api/staff/staff-users/non-existent-guid", headers=headers) + + assert response.status_code == HTTPStatus.NOT_FOUND + + +def test_get_staff_user_unauthorized(client, session): + """Test fetching staff user without authentication.""" + response = client.get("/api/staff/staff-users/some-guid") + + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_create_staff_user_success(client, session, jwt): + """Test creating a staff user and assigning Keycloak role.""" + email = fake.email() + group_name = "EAO_VIEW" + + # Mock Keycloak service responses + mock_keycloak_user = { + "username": f"{fake.user_name()}@idir", + "firstName": fake.first_name(), + "lastName": fake.last_name(), + "email": email + } + + with patch('submit_api.services.staff_user_service.KeycloakService.get_user_by_email') as mock_get_user, \ + patch('submit_api.services.staff_user_service.KeycloakService.get_group_id_by_path') as mock_get_group, \ + patch('submit_api.services.staff_user_service.KeycloakService.update_user_group') as mock_update_group: + + mock_get_user.return_value = mock_keycloak_user + mock_get_group.return_value = "group-id-123" + mock_update_group.return_value = None + + payload = { + "email": email, + "group_name": group_name + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.post("/api/staff/staff-users/", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.CREATED + data = response.get_json() + assert "message" in data + assert email in data["message"] + assert group_name in data["message"] + + # Verify Keycloak methods were called + mock_get_user.assert_called_once_with(email) + mock_get_group.assert_called_once_with(group_name) + mock_update_group.assert_called_once() + + +def test_create_staff_user_missing_email(client, session, jwt): + """Test creating staff user without email.""" + payload = { + "group_name": "EAO_VIEW" + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.post("/api/staff/staff-users/", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.get_json() + assert "message" in data + + +def test_create_staff_user_missing_group_name(client, session, jwt): + """Test creating staff user without group name.""" + payload = { + "email": fake.email() + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.post("/api/staff/staff-users/", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.get_json() + assert "message" in data + + +def test_create_staff_user_keycloak_error(client, session, jwt): + """Test creating staff user when Keycloak service fails.""" + email = fake.email() + group_name = "EAO_VIEW" + + with patch('submit_api.services.staff_user_service.KeycloakService.get_user_by_email') as mock_get_user: + mock_get_user.side_effect = Exception("Keycloak connection error") + + payload = { + "email": email, + "group_name": group_name + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.post("/api/staff/staff-users/", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + data = response.get_json() + assert "message" in data + + +def test_create_staff_user_unauthorized(client, session): + """Test creating staff user without authentication.""" + payload = { + "email": fake.email(), + "group_name": "EAO_VIEW" + } + + response = client.post("/api/staff/staff-users/", json=payload) + + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_create_staff_user_without_manage_users_role(client, session, jwt): + """Test creating staff user without MANAGE_USERS role.""" + # Use proponent role which doesn't have MANAGE_USERS permission + payload = { + "email": fake.email(), + "group_name": "EAO_VIEW" + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.proponent_role) + response = client.post("/api/staff/staff-users/", json=payload, headers=headers) + + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_get_staff_user_with_existing_user(client, session, jwt): + """Test fetching staff user when user exists with staff_user relationship.""" + auth_guid = f"{fake.user_name()}@idir" + user = factory_user_model(auth_guid=auth_guid, user_type=UserType.STAFF, session=session) + + from submit_api.models.staff_user import StaffUser + first_name = fake.first_name() + last_name = fake.last_name() + work_email = fake.email() + + staff_user_data = { + 'first_name': first_name, + 'last_name': last_name, + 'work_email_address': work_email, + 'user_id': user.id + } + StaffUser.create_staff_user(staff_user_data, session=session) + session.commit() + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + response = client.get(f"/api/staff/staff-users/{auth_guid}", headers=headers) + + assert response.status_code == HTTPStatus.OK + data = response.get_json() + assert data["first_name"] == first_name + assert data["last_name"] == last_name + assert data["work_email_address"] == work_email + assert data["user_id"] == user.id + + +def test_create_staff_user_idempotent(client, session, jwt): + """Test creating staff user multiple times is idempotent.""" + email = fake.email() + group_name = "EAO_VIEW" + username = f"{fake.user_name()}@idir" + + mock_keycloak_user = { + "username": username, + "firstName": fake.first_name(), + "lastName": fake.last_name(), + "email": email + } + + with patch('submit_api.services.staff_user_service.KeycloakService.get_user_by_email') as mock_get_user, \ + patch('submit_api.services.staff_user_service.KeycloakService.get_group_id_by_path') as mock_get_group, \ + patch('submit_api.services.staff_user_service.KeycloakService.update_user_group') as mock_update_group: + + mock_get_user.return_value = mock_keycloak_user + mock_get_group.return_value = "group-id-123" + mock_update_group.return_value = None + + payload = { + "email": email, + "group_name": group_name + } + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) + + # First creation + response1 = client.post("/api/staff/staff-users/", json=payload, headers=headers) + assert response1.status_code == HTTPStatus.CREATED + + # Second creation - should still succeed (idempotent) + response2 = client.post("/api/staff/staff-users/", json=payload, headers=headers) + assert response2.status_code == HTTPStatus.CREATED diff --git a/submit-web/src/hooks/api/constants.ts b/submit-web/src/hooks/api/constants.ts index 75b6bc304..629df7569 100644 --- a/submit-web/src/hooks/api/constants.ts +++ b/submit-web/src/hooks/api/constants.ts @@ -14,7 +14,7 @@ export const QUERY_KEY = Object.freeze({ SUBMISSIONS: "submissions", USERS: "users", PACKAGE_VERSIONS: "package-versions", - STAFF_USER: "staff/staff-user", + STAFF_USER: "staff/staff-users", ACTIVITY_LOGS: "activity-logs", SUBMISSION_VERSIONS: "submission-versions", INVITATION: "invitation", diff --git a/submit-web/src/hooks/api/useStaffUser.ts b/submit-web/src/hooks/api/useStaffUser.ts index 2da01d402..d4f2c4279 100644 --- a/submit-web/src/hooks/api/useStaffUser.ts +++ b/submit-web/src/hooks/api/useStaffUser.ts @@ -11,12 +11,12 @@ type CreateStaffRequest = { }; const fetchStaffUserByGUID = (id?: string) => { - return submitRequest({ url: `staff/staff-user/${id}` }); + return submitRequest({ url: `staff/staff-users/${id}` }); }; const addStaffUser = (data: CreateStaffRequest) => { return submitRequest({ - url: "staff/staff-user", + url: "staff/staff-users", method: "post", data, });