Files
finance/backend/tests/test_review_queue_batch.py
k1nq 970fe9dacf
Gitea Actions CI / detect (push) Successful in 3s
Gitea Actions CI / Compose & nginx validation (push) Failing after 14s
Gitea Actions CI / Frontend (lint / type / test / build) (push) Successful in 45s
Gitea Actions CI / Backend (lint / type / test / build) (push) Successful in 1m31s
Refactor code for improved readability and consistency across multiple files
2026-09-23 15:22:26 +05:00

176 lines
5.9 KiB
Python

from fastapi.testclient import TestClient
def bootstrap(client: TestClient) -> str:
invite = client.post(
"/api/v1/admin/invitations",
headers={"X-Admin-Token": "test-admin-token"},
json={"email": "Queue@Example.com"},
)
assert invite.status_code == 201, invite.text
accepted = client.post(
"/api/v1/auth/invitations/accept",
json={
"token": invite.json()["token"],
"password": "a-long-password-for-tests",
"fullName": "Test Person",
"locale": "en",
},
)
assert accepted.status_code == 201, accepted.text
return client.cookies["csrf_token"]
def create_account(client: TestClient, csrf: str) -> str:
response = client.post(
"/api/v1/accounts",
headers={"X-CSRF-Token": csrf},
json={
"name": "Wallet",
"type": "cash",
"openedOn": "2026-02-01",
"openingBalanceKopecks": 100_000,
},
)
assert response.status_code == 201, response.text
return response.json()["id"]
def create_expense(client: TestClient, csrf: str, account_id: str, description: str) -> str:
response = client.post(
"/api/v1/operations",
headers={"X-CSRF-Token": csrf},
json={
"kind": "expense",
"accountId": account_id,
"amountKopecks": 1_500,
"occurredOn": "2026-02-10",
"description": description,
},
)
assert response.status_code == 201, response.text
return response.json()["id"]
def category_ids(client: TestClient) -> dict[str, str]:
response = client.get(
"/api/v1/budget/category-definitions", params={"includeArchived": "false"}
)
assert response.status_code == 200, response.text
return {
item["systemKey"]: item["id"] for item in response.json() if item["systemKey"] is not None
}
def operations_by_id(client: TestClient) -> dict[str, dict]:
response = client.get("/api/v1/operations", params={"kind": "expense", "limit": 200})
assert response.status_code == 200, response.text
return {item["id"]: item for item in response.json()["items"]}
def test_batch_assigns_picked_categories_and_moves_leftovers_to_others(
client: TestClient,
) -> None:
csrf = bootstrap(client)
account_id = create_account(client, csrf)
categories = category_ids(client)
first = create_expense(client, csrf, account_id, "Coffee")
second = create_expense(client, csrf, account_id, "Groceries")
response = client.post(
"/api/v1/operations/categorize-batch",
headers={"X-CSRF-Token": csrf},
json={
"items": [
{"operationId": first, "categoryId": categories["groceries"]},
{"operationId": second},
]
},
)
assert response.status_code == 200, response.text
assert response.json() == {"updated": [first, second], "skipped": []}
operations = operations_by_id(client)
assert operations[first]["categoryKey"] == "groceries"
assert operations[second]["categoryKey"] == "others"
assert operations[second]["categoryName"] == "Остальное"
left = client.get("/api/v1/operations", params={"uncategorized": "true"})
assert left.json()["items"] == []
def test_batch_never_overwrites_existing_categories_or_voided_operations(
client: TestClient,
) -> None:
csrf = bootstrap(client)
account_id = create_account(client, csrf)
categories = category_ids(client)
categorized = create_expense(client, csrf, account_id, "Already done")
voided = create_expense(client, csrf, account_id, "Voided")
pending = create_expense(client, csrf, account_id, "Pending")
patch = client.patch(
f"/api/v1/operations/{categorized}",
headers={"X-CSRF-Token": csrf},
json={"categoryId": categories["groceries"]},
)
assert patch.status_code == 200, patch.text
void = client.post(
f"/api/v1/operations/{voided}/void",
headers={"X-CSRF-Token": csrf},
json={"reason": "duplicate"},
)
assert void.status_code == 200, void.text
response = client.post(
"/api/v1/operations/categorize-batch",
headers={"X-CSRF-Token": csrf},
json={
"items": [
{"operationId": categorized, "categoryId": categories["cafes"]},
{"operationId": voided, "categoryId": categories["cafes"]},
{"operationId": pending, "categoryId": categories["cafes"]},
]
},
)
assert response.status_code == 200, response.text
assert response.json() == {"updated": [pending], "skipped": [categorized, voided]}
operations = operations_by_id(client)
assert operations[categorized]["categoryKey"] == "groceries"
assert operations[pending]["categoryKey"] == "cafes"
def test_batch_is_atomic_when_one_category_is_unknown(client: TestClient) -> None:
csrf = bootstrap(client)
account_id = create_account(client, csrf)
first = create_expense(client, csrf, account_id, "First")
second = create_expense(client, csrf, account_id, "Second")
response = client.post(
"/api/v1/operations/categorize-batch",
headers={"X-CSRF-Token": csrf},
json={
"items": [
{"operationId": first, "categoryId": "00000000-0000-0000-0000-000000000000"},
{"operationId": second},
]
},
)
assert response.status_code == 404, response.text
assert response.json()["error"]["code"] == "BUDGET_CATEGORY_NOT_FOUND"
operations = operations_by_id(client)
assert operations[first]["categoryId"] is None
assert operations[second]["categoryId"] is None
def test_batch_rejects_empty_payload(client: TestClient) -> None:
csrf = bootstrap(client)
response = client.post(
"/api/v1/operations/categorize-batch",
headers={"X-CSRF-Token": csrf},
json={"items": []},
)
assert response.status_code == 422, response.text