From a718827c12ea84e3ecf3fade6d5cb790b04c6225 Mon Sep 17 00:00:00 2001 From: Raykov-MS Date: Fri, 15 May 2026 18:42:05 +0300 Subject: [PATCH 1/2] add and fix validations on forms --- api/src/api/v1/forms.py | 55 +++++++++++++----- api/src/api/v1/projects.py | 7 ++- api/src/repository/sheet_repository.py | 16 +++--- api/src/services/sheet_service.py | 14 ++--- api/tests/unit/test_query_validation.py | 74 +++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 api/tests/unit/test_query_validation.py diff --git a/api/src/api/v1/forms.py b/api/src/api/v1/forms.py index 2dd883b..a52e5a2 100644 --- a/api/src/api/v1/forms.py +++ b/api/src/api/v1/forms.py @@ -15,6 +15,8 @@ from src.api.v1.deps import get_current_active_user_with_set_db from src.db.session import get_db router = APIRouter(prefix="/form", tags=["forms"]) +SHEETS_WITH_SECTIONS = {"AHR", "CAP", "OPER"} +FORM1_DIRECTION_REQUIRED_SHEETS = {"AHR", "CAP"} def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]: @@ -28,6 +30,31 @@ def _rows_to_sheet_response(result: list[tuple]) -> list[SheetResponse]: ] +def _parse_sections_csv(sections: Optional[str]) -> Optional[list[str]]: + if sections is None: + return None + parsed = [section.strip() for section in sections.split(",") if section.strip()] + return parsed or None + + +def _validate_sheet_query_params( + form_type_code: FormTypeEnum, + sheet: str, + direction: Optional[DirectionSchemaEnum], + sections: Optional[list[str]], +) -> None: + if form_type_code == FormTypeEnum.FORM_1 and sheet in FORM1_DIRECTION_REQUIRED_SHEETS and not direction: + raise HTTPException(400, "direction обязателен для FORM_1 листов AHR/CAP") + + if direction and not ( + form_type_code == FormTypeEnum.FORM_1 and sheet in FORM1_DIRECTION_REQUIRED_SHEETS + ): + raise HTTPException(400, "direction допустим только для FORM_1 листов AHR/CAP") + + if sections and sheet not in SHEETS_WITH_SECTIONS: + raise HTTPException(400, f"sections не поддерживается для листа {sheet}") + + @router.get("/") async def get_forms( offset: int = 0, @@ -51,7 +78,7 @@ async def get_forms( ) -@router.get("{form_id}/sheets") +@router.get("/{form_id}/sheets") async def get_form_sheets( form_id: int, db: AsyncSession = Depends(get_db), @@ -81,8 +108,7 @@ async def get_sheet( db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(get_current_active_user_with_set_db), ) -> BaseListResponse[SheetResponse]: - if sections is not None: - sections = sections.split(",") + sections = _parse_sections_csv(sections) t0 = time.perf_counter() bf_service = BudgetFormService(db) sheet_service = SheetService(db) @@ -96,10 +122,9 @@ async def get_sheet( raise HTTPException(404, f"Форма {form_id} не найдена") if sheet not in form.form_type.sheet_list: raise HTTPException(404, f"Лист {sheet} формы {form_id} не найден") + _validate_sheet_query_params(form.form_type.code, sheet, direction, sections) if sections and (rem_sects := set(sections) - set(form.form_type.section_list)): - raise HTTPException(404, f"Секций {', '.join(rem_sects)} формы {form_id} не найден") - if form.form_type.code == FormTypeEnum.FORM_1 and not direction: - raise HTTPException(400, f"Форма типа {form.form_type.code} должна содержать в запросе direction") + raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}") result = await sheet_service.get( form_id=form_id, @@ -126,8 +151,7 @@ async def update_cell( db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(get_current_active_user_with_set_db), ) -> BaseListResponse[SheetResponse]: - if sections is not None: - sections = sections.split(",") + sections = _parse_sections_csv(sections) t0 = time.perf_counter() bf_service = BudgetFormService(db) sheet_service = SheetService(db) @@ -141,8 +165,9 @@ async def update_cell( raise HTTPException(404, f"Форма {form_id} не найдена") if sheet not in form.form_type.sheet_list: raise HTTPException(404, f"Лист {sheet} формы {form_id} не найден") + _validate_sheet_query_params(form.form_type.code, sheet, direction, sections) if sections and (rem_sects := set(sections) - set(form.form_type.section_list)): - raise HTTPException(404, f"Секций {', '.join(rem_sects)} формы {form_id} не найден") + raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}") budget_line_service = BudgetLineService(db) budget_line = await budget_line_service.get(budget_line_id=cell_body.line_id, user=current_user) @@ -181,8 +206,7 @@ async def update_cells( db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(get_current_active_user_with_set_db), ) -> BaseListResponse[SheetResponse]: - if sections is not None: - sections = sections.split(",") + sections = _parse_sections_csv(sections) t0 = time.perf_counter() bf_service = BudgetFormService(db) sheet_service = SheetService(db) @@ -196,8 +220,9 @@ async def update_cells( raise HTTPException(404, f"Форма {form_id} не найдена") if sheet not in form.form_type.sheet_list: raise HTTPException(404, f"Лист {sheet} формы {form_id} не найден") + _validate_sheet_query_params(form.form_type.code, sheet, direction, sections) if sections and (rem_sects := set(sections) - set(form.form_type.section_list)): - raise HTTPException(404, f"Секций {', '.join(rem_sects)} формы {form_id} не найден") + raise HTTPException(400, f"Недопустимые sections: {', '.join(sorted(rem_sects))}") budget_line_service = BudgetLineService(db) budget_lines = await budget_line_service.get_list( @@ -246,6 +271,7 @@ async def add_line( raise HTTPException(404, f"Форма {form_id} не найдена") if sheet not in form.form_type.sheet_list: raise HTTPException(404, f"Лист {sheet} формы {form_id} не найден") + _validate_sheet_query_params(form.form_type.code, sheet, body.direction, None) result = await sheet_service.add_line( form_id=form_id, @@ -278,7 +304,7 @@ async def delete_line( sheet: str, row_id: int, response: Response, - direction: str | None = None, + direction: Optional[DirectionSchemaEnum] = None, db: AsyncSession = Depends(get_db), current_user: AppUser = Depends(get_current_active_user_with_set_db), ): @@ -295,12 +321,13 @@ async def delete_line( raise HTTPException(404, f"Форма {form_id} не найдена") if sheet not in form.form_type.sheet_list: raise HTTPException(404, f"Лист {sheet} формы {form_id} не найден") + _validate_sheet_query_params(form.form_type.code, sheet, direction, None) result = await sheet_service.delete_line( form_id=form_id, sheet=sheet, row_id=row_id, - direction=direction, + direction=direction.value if direction else None, user=current_user, ) db_ms = (time.perf_counter() - t0) * 1000 diff --git a/api/src/api/v1/projects.py b/api/src/api/v1/projects.py index 815d1f5..326a50c 100644 --- a/api/src/api/v1/projects.py +++ b/api/src/api/v1/projects.py @@ -21,12 +21,17 @@ from src.services.project_service import ProjectService router = APIRouter(tags=["forms"]) +FORM3_ALLOWED_SECTIONS = {"q1", "q2", "q3", "q4", "year"} def _parse_sections(sections: Optional[str]) -> Optional[list[str]]: if not sections: return None - return [s.strip() for s in sections.split(",") if s.strip()] + parsed = [s.strip() for s in sections.split(",") if s.strip()] + invalid = sorted(set(parsed) - FORM3_ALLOWED_SECTIONS) + if invalid: + raise HTTPException(400, f"Недопустимые sections для FORM_3: {', '.join(invalid)}") + return parsed def _rows_to_payload(rows: list[tuple]) -> list[dict]: diff --git a/api/src/repository/sheet_repository.py b/api/src/repository/sheet_repository.py index 50915a3..12b5e2b 100644 --- a/api/src/repository/sheet_repository.py +++ b/api/src/repository/sheet_repository.py @@ -32,14 +32,14 @@ class SheetRepository: self, form_id: int, sheet: str, - direction: str, + direction: str | None, sections: list[str] | None = None, ) -> list[tuple]: if any( [ not isinstance(form_id, int), sheet not in ACCEPTABLE_SHEETS, - direction not in [s.value for s in DirectionEnum], + direction is not None and direction not in [s.value for s in DirectionEnum], sections and (set(sections) - ACCEPTABLE_SECTIONS), ] ): @@ -72,8 +72,8 @@ class SheetRepository: self, form_id: int, sheet: str, - direction: str, - sections: list[str], + direction: str | None, + sections: list[str] | None, line_id: int, column: str, value: str | int | float | None = None, @@ -83,7 +83,7 @@ class SheetRepository: [ not isinstance(form_id, int), sheet not in ACCEPTABLE_SHEETS, - direction not in [s.value for s in DirectionEnum], + direction is not None and direction not in [s.value for s in DirectionEnum], sections and (set(sections) - ACCEPTABLE_SECTIONS), ] ): @@ -118,8 +118,8 @@ class SheetRepository: self, form_id: int, sheet: str, - direction: str, - sections: list[str], + direction: str | None, + sections: list[str] | None, changes: list[dict], user_id: int = None, ) -> list[tuple]: @@ -127,7 +127,7 @@ class SheetRepository: [ not isinstance(form_id, int), sheet not in ACCEPTABLE_SHEETS, - direction not in [s.value for s in DirectionEnum], + direction is not None and direction not in [s.value for s in DirectionEnum], sections and (set(sections) - ACCEPTABLE_SECTIONS), ] ): diff --git a/api/src/services/sheet_service.py b/api/src/services/sheet_service.py index 2345d72..3c9ba78 100644 --- a/api/src/services/sheet_service.py +++ b/api/src/services/sheet_service.py @@ -15,7 +15,7 @@ class SheetService: async def get( self, sheet: str, - direction: str, + direction: str | None, sections: list[str] | None = None, form_id: int | None = None, form: BudgetForm | None = None @@ -31,8 +31,8 @@ class SheetService: self, form_id: int, sheet: str, - direction: str, - sections: list[str], + direction: str | None, + sections: list[str] | None, line_id: int, column: str, value: str | int | float | None = None, @@ -53,8 +53,8 @@ class SheetService: self, form_id: int, sheet: str, - direction: str, - sections: list[str], + direction: str | None, + sections: list[str] | None, changes: list[dict], user: AppUser | None = None, ) -> list[tuple]: @@ -71,8 +71,8 @@ class SheetService: self, form_id: int, sheet: str, - direction: str, - sections: list[str], + direction: str | None, + sections: list[str] | None, line_id: int, column: str, value: str | int | float | None = None, diff --git a/api/tests/unit/test_query_validation.py b/api/tests/unit/test_query_validation.py new file mode 100644 index 0000000..e802d77 --- /dev/null +++ b/api/tests/unit/test_query_validation.py @@ -0,0 +1,74 @@ +import pytest +from fastapi import HTTPException + +from src.api.v1.forms import _parse_sections_csv, _validate_sheet_query_params +from src.api.v1.projects import _parse_sections +from src.db.models.form_type import FormTypeEnum +from src.domain.schemas import DirectionSchemaEnum + + +def test_parse_sections_csv_returns_none_for_empty_values(): + assert _parse_sections_csv(None) is None + assert _parse_sections_csv("") is None + assert _parse_sections_csv(" , , ") is None + + +def test_parse_sections_csv_splits_and_trims(): + assert _parse_sections_csv("plan, q1, q2 ") == ["plan", "q1", "q2"] + + +def test_validate_sheet_query_requires_direction_for_form1_ahr_cap(): + with pytest.raises(HTTPException) as exc: + _validate_sheet_query_params(FormTypeEnum.FORM_1, "AHR", None, None) + assert exc.value.status_code == 400 + + +def test_validate_sheet_query_rejects_direction_for_non_form1_or_non_ahr_cap(): + with pytest.raises(HTTPException) as exc1: + _validate_sheet_query_params( + FormTypeEnum.FORM_2, + "AHR", + DirectionSchemaEnum.SUPPORT, + None, + ) + assert exc1.value.status_code == 400 + + with pytest.raises(HTTPException) as exc2: + _validate_sheet_query_params( + FormTypeEnum.FORM_1, + "SMETA", + DirectionSchemaEnum.SUPPORT, + None, + ) + assert exc2.value.status_code == 400 + + +def test_validate_sheet_query_rejects_sections_for_non_main_sheets(): + with pytest.raises(HTTPException) as exc: + _validate_sheet_query_params(FormTypeEnum.FORM_1, "SMETA", None, ["q1"]) + assert exc.value.status_code == 400 + + +def test_validate_sheet_query_accepts_valid_combinations(): + _validate_sheet_query_params( + FormTypeEnum.FORM_1, + "AHR", + DirectionSchemaEnum.SUPPORT, + ["plan", "q1"], + ) + _validate_sheet_query_params( + FormTypeEnum.FORM_2, + "AHR", + None, + ["plan", "q1"], + ) + + +def test_parse_form3_sections_accepts_supported_values(): + assert _parse_sections("q1,q2,year") == ["q1", "q2", "year"] + + +def test_parse_form3_sections_rejects_invalid_values(): + with pytest.raises(HTTPException) as exc: + _parse_sections("q1,q5") + assert exc.value.status_code == 400 From 5cd7232eb23ffbea74403d02d45ac45225abbe6b Mon Sep 17 00:00:00 2001 From: Raykov-MS Date: Mon, 18 May 2026 16:12:16 +0300 Subject: [PATCH 2/2] add integrated tests --- .../test_db_functions_integration.py | 310 ++++++++++++++++++ api/tests/integration/test_forms_api_smoke.py | 4 +- 2 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 api/tests/integration/test_db_functions_integration.py diff --git a/api/tests/integration/test_db_functions_integration.py b/api/tests/integration/test_db_functions_integration.py new file mode 100644 index 0000000..5ed3e09 --- /dev/null +++ b/api/tests/integration/test_db_functions_integration.py @@ -0,0 +1,310 @@ +import uuid + +import pytest + + +def _auth_headers(tokens: dict) -> dict: + return {"Authorization": f"Bearer {tokens['access_token']}"} + + +def _pick_form_and_sheet(client, admin_tokens) -> tuple[int, str, str | None]: + response = client.get("/api/v1/form/", headers=_auth_headers(admin_tokens)) + assert response.status_code == 200 + forms = response.json().get("result") or [] + + for form in forms: + form_id = form.get("id") + form_type = form.get("form_type_code") + if not form_id or not form_type: + continue + + sheets_response = client.get( + f"/api/v1/form/{form_id}/sheets", + headers=_auth_headers(admin_tokens), + ) + if sheets_response.status_code != 200: + continue + + sheets = (sheets_response.json().get("result") or {}).get("sheets") or [] + if "AHR" not in sheets: + continue + + direction = "Support" if form_type == "FORM_1" else None + return int(form_id), "AHR", direction + + pytest.skip("В тестовой БД не найдена форма с листом AHR") + + +def _pick_form_with_sheet( + client, admin_tokens, target_sheet: str, form_type_code: str | None = None +) -> tuple[int, str]: + response = client.get("/api/v1/form/", headers=_auth_headers(admin_tokens)) + assert response.status_code == 200 + forms = response.json().get("result") or [] + for form in forms: + form_id = form.get("id") + if not form_id: + continue + if form_type_code and form.get("form_type_code") != form_type_code: + continue + sheets_response = client.get( + f"/api/v1/form/{form_id}/sheets", + headers=_auth_headers(admin_tokens), + ) + if sheets_response.status_code != 200: + continue + sheets = (sheets_response.json().get("result") or {}).get("sheets") or [] + if target_sheet in sheets: + return int(form_id), target_sheet + pytest.skip(f"Не найдена форма с листом {target_sheet}") + + +def _pick_input_line_id(client, admin_tokens, form_id: int, sheet: str, direction: str | None) -> int: + url = f"/api/v1/form/{form_id}/sheet/{sheet}" + params = [] + if direction: + params.append(f"direction={direction}") + if params: + url = f"{url}?{'&'.join(params)}" + + response = client.get(url, headers=_auth_headers(admin_tokens)) + assert response.status_code == 200 + rows = response.json().get("result") or [] + for row in rows: + data = row.get("data") or {} + if row.get("row_type") == "INPUT" and data.get("line_id"): + return int(data["line_id"]) + + pytest.skip("Не найден INPUT line_id для проверки upd_form_cell") + + +def test_backend_calls_v_form_view_via_sheet_endpoint(client, admin_tokens): + form_id, sheet, direction = _pick_form_and_sheet(client, admin_tokens) + url = f"/api/v1/form/{form_id}/sheet/{sheet}" + if direction: + url = f"{url}?direction={direction}" + + response = client.get(url, headers=_auth_headers(admin_tokens)) + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload.get("result"), list) + if payload["result"]: + first_row = payload["result"][0] + assert "row_type" in first_row + assert "data" in first_row + + +def test_backend_calls_upd_form_cell_and_maps_sql_validation_error(client, admin_tokens): + form_id, sheet, direction = _pick_form_and_sheet(client, admin_tokens) + line_id = _pick_input_line_id(client, admin_tokens, form_id, sheet, direction) + + url = f"/api/v1/form/{form_id}/sheet/{sheet}/cell" + if direction: + url = f"{url}?direction={direction}" + + response = client.patch( + url, + json={"line_id": line_id, "column": "bad.scope", "value": 1}, + headers=_auth_headers(admin_tokens), + ) + + # SQL-функция отдает prefixed validation error. + assert response.status_code in (400, 422) + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + assert isinstance(payload["message"], str) + + +def test_backend_calls_form3_add_project_and_add_line_functions(client, admin_tokens): + create = client.post( + "/api/v1/projects", + json={"name": f"ITEST_DB_FUNC_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert create.status_code == 200 + project_payload = create.json() + assert "project_id" in project_payload + assert "limit_report_id" in project_payload + assert "current_expenses_report_id" in project_payload + + project_id = int(project_payload["project_id"]) + add_line = client.post( + f"/api/v1/projects/{project_id}/report/2026/LIMIT/line", + json={"expense_item_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert add_line.status_code == 200 + rows = add_line.json() + assert isinstance(rows, list) + + +def test_forms_validation_invalid_sections_returns_400(client, admin_tokens): + form_id, sheet = _pick_form_with_sheet(client, admin_tokens, "AHR") + response = client.get( + f"/api/v1/form/{form_id}/sheet/{sheet}?sections=bad_section", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + + +def test_forms_validation_empty_sections_csv_is_ignored(client, admin_tokens): + form_id, sheet, direction = _pick_form_and_sheet(client, admin_tokens) + url = f"/api/v1/form/{form_id}/sheet/{sheet}?sections= , , " + if direction: + url += f"&direction={direction}" + response = client.get(url, headers=_auth_headers(admin_tokens)) + assert response.status_code == 200 + payload = response.json() + assert isinstance(payload.get("result"), list) + + +def test_forms_validation_direction_required_for_form1_ahr(client, admin_tokens): + form_id, sheet = _pick_form_with_sheet(client, admin_tokens, "AHR", form_type_code="FORM_1") + response = client.get( + f"/api/v1/form/{form_id}/sheet/{sheet}", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + assert "direction" in payload["message"].lower() + + +def test_forms_validation_direction_case_sensitive(client, admin_tokens): + form_id, sheet = _pick_form_with_sheet(client, admin_tokens, "AHR", form_type_code="FORM_1") + response = client.get( + f"/api/v1/form/{form_id}/sheet/{sheet}?direction=support", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 422 + + +def test_forms_validation_direction_forbidden_on_non_form1(client, admin_tokens): + form_id, sheet = _pick_form_with_sheet(client, admin_tokens, "AHR", form_type_code="FORM_2") + response = client.get( + f"/api/v1/form/{form_id}/sheet/{sheet}?direction=Support", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + assert "direction" in payload["message"].lower() + + +def test_form3_validation_invalid_sections_returns_400(client, admin_tokens): + create = client.post( + "/api/v1/projects", + json={"name": f"ITEST_F3_SECT_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert create.status_code == 200 + project_id = int(create.json()["project_id"]) + response = client.get( + f"/api/v1/projects/{project_id}/report/2026/LIMIT?sections=q1,q5", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 400 + payload = response.json() + assert isinstance(payload, dict) + assert "message" in payload + + +def test_form3_validation_empty_sections_csv_is_ignored(client, admin_tokens): + create = client.post( + "/api/v1/projects", + json={"name": f"ITEST_F3_EMPTY_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert create.status_code == 200 + project_id = int(create.json()["project_id"]) + response = client.get( + f"/api/v1/projects/{project_id}/report/2026/LIMIT?sections= , , ", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 200 + assert isinstance(response.json().get("result"), list) + + +def test_form3_validation_duplicate_sections_is_allowed(client, admin_tokens): + create = client.post( + "/api/v1/projects", + json={"name": f"ITEST_F3_DUP_{uuid.uuid4().hex[:6]}", "year": 2026, "branch_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert create.status_code == 200 + project_id = int(create.json()["project_id"]) + response = client.get( + f"/api/v1/projects/{project_id}/report/2026/LIMIT?sections=q1,q1", + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 200 + assert isinstance(response.json().get("result"), list) + + +def test_form3_add_project_invalid_name_returns_422(client, admin_tokens): + # Пробел запрещён regex-правилом AddProjectBody. + response = client.post( + "/api/v1/projects", + json={"name": "INVALID NAME", "year": 2026, "branch_id": 1}, + headers=_auth_headers(admin_tokens), + ) + assert response.status_code == 422 + + +def test_health_and_ready_endpoints(client): + healthz = client.get("/healthz") + assert healthz.status_code == 200 + health_payload = healthz.json() + assert health_payload.get("status") == "ok" + + readyz = client.get("/readyz") + assert readyz.status_code in (200, 503) + ready_payload = readyz.json() + assert "status" in ready_payload + assert "db_status" in ready_payload + assert "mv_expense_item_tree" in ready_payload + + +def test_admin_refresh_tree_requires_admin_role(client, admin_tokens): + admin_resp = client.post( + "/api/v1/admin/refresh-tree", + headers=_auth_headers(admin_tokens), + ) + assert admin_resp.status_code in (200, 503) + + uname = f"itest_non_admin_{uuid.uuid4().hex[:6]}" + create_user = client.put( + "/api/v1/users/", + json={ + "email": f"{uname}@example.com", + "username": uname, + "password": "pass123", + "full_name": "Integration User", + "role_id": 2, + }, + headers=_auth_headers(admin_tokens), + ) + if create_user.status_code != 200: + pytest.skip("Не удалось создать non-admin пользователя в текущей БД") + + login_resp = client.post("/api/v1/auth/login", json={"username": uname, "password": "pass123"}) + if login_resp.status_code != 200: + pytest.skip("Не удалось залогинить non-admin пользователя в текущей БД") + user_tokens = login_resp.json() + + non_admin_resp = client.post( + "/api/v1/admin/refresh-tree", + headers=_auth_headers(user_tokens), + ) + assert non_admin_resp.status_code == 403 + + +def test_admin_refresh_tree_requires_auth(client): + response = client.post("/api/v1/admin/refresh-tree") + assert response.status_code in (401, 403) diff --git a/api/tests/integration/test_forms_api_smoke.py b/api/tests/integration/test_forms_api_smoke.py index d7b0d4f..5bd3997 100644 --- a/api/tests/integration/test_forms_api_smoke.py +++ b/api/tests/integration/test_forms_api_smoke.py @@ -21,8 +21,8 @@ def test_forms_sheets_not_found_error_shape(client, admin_tokens): assert response.status_code == 404 payload = response.json() assert isinstance(payload, dict) - assert "detail" in payload - assert isinstance(payload["detail"], str) + assert "message" in payload + assert isinstance(payload["message"], str) def test_forms_sheet_get_not_found_error_shape(client, admin_tokens):