Files
finance/backend/tests/test_imports.py
T
k1nq d582f15681 feat: add tests for import polling, login page, money utilities, and query keys
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
2026-09-14 09:26:33 +05:00

284 lines
9.4 KiB
Python

import uuid
from contextlib import contextmanager
from datetime import date
import pytest
from sqlalchemy.orm import Session
from app.errors import AppError
from app.identity.models import User
from app.imports import service
from app.imports.adapters.base import SourceRow
from app.imports.models import (
DedupCandidate,
ParseRun,
SourceRecord,
SourceRecordLink,
StatementDocument,
)
from app.ledger.models import Operation
from app.ledger.service import create_account
from app.spaces.service import create_space
def seed(db: Session):
user = User(email=f"{uuid.uuid4().hex}@example.com", password_hash="unused")
db.add(user)
db.flush()
space = create_space(db, user=user, name="Imports")
account = create_account(
db,
space_id=space.id,
user=user,
name="Debit",
kind="debit",
opened_on=date(2026, 1, 1),
)
return user, space, account
def ready_document(db: Session):
user, space, account = seed(db)
document = StatementDocument(
space_id=space.id,
account_id=account.id,
original_filename="synthetic.xlsx",
size_bytes=100,
content_sha256=uuid.uuid4().hex + uuid.uuid4().hex,
object_key=uuid.uuid4().hex,
status="ready",
reconciliation={"full": True, "matched": True},
uploaded_by_user_id=user.id,
)
db.add(document)
db.flush()
run = ParseRun(
space_id=space.id,
document_id=document.id,
adapter_id="alfa_debit_xlsx",
status="succeeded",
)
db.add(run)
db.flush()
document.active_parse_run_id = run.id
source = SourceRecord(
space_id=space.id,
document_id=document.id,
parse_run_id=run.id,
occurred_on=date(2026, 1, 5),
amount_kopecks=12_345,
direction="out",
row_ref="sheet0:row2",
record_fp=uuid.uuid4().hex,
)
db.add(source)
db.flush()
return user, space, document
def test_confirm_and_revert_are_idempotent(db: Session) -> None:
user, space, document = ready_document(db)
service.confirm_document(db, space_id=space.id, document_id=document.id, user=user)
service.confirm_document(db, space_id=space.id, document_id=document.id, user=user)
db.flush()
assert db.query(Operation).filter_by(space_id=space.id).count() == 1
assert db.query(SourceRecordLink).filter_by(space_id=space.id).count() == 1
service.revert_document(db, space_id=space.id, document_id=document.id, user=user)
service.revert_document(db, space_id=space.id, document_id=document.id, user=user)
db.flush()
assert db.query(Operation).filter_by(space_id=space.id).count() == 0
assert db.query(SourceRecordLink).filter_by(space_id=space.id).count() == 0
def test_confirm_rejects_arithmetic_mismatch(db: Session) -> None:
user, space, document = ready_document(db)
document.reconciliation = {"full": True, "matched": False}
with pytest.raises(AppError) as exc:
service.confirm_document(db, space_id=space.id, document_id=document.id, user=user)
assert exc.value.code == "IMPORT_RECONCILIATION_MISMATCH"
def test_confirmed_import_must_be_reverted_before_reparse(db: Session) -> None:
user, space, document = ready_document(db)
service.confirm_document(db, space_id=space.id, document_id=document.id, user=user)
with pytest.raises(AppError) as exc:
service.prepare_reparse(db, space_id=space.id, document_id=document.id, user=user)
assert exc.value.code == "IMPORT_REPARSE_CONFIRMED"
def test_invalid_direction_is_critical() -> None:
source = service._source_from_row(
space_id=uuid.uuid4(),
document_id=uuid.uuid4(),
run_id=uuid.uuid4(),
row=SourceRow(
external_id=None,
occurred_on=date(2026, 1, 1),
posted_on=None,
amount_kopecks=100,
direction="sideways",
description=None,
mcc=None,
bank_category=None,
location=None,
row_ref="row1",
),
)
assert source.critical is True
assert source.field_warnings == {"codes": ["direction_invalid"]}
def test_unexpected_parser_failure_reaches_terminal_state(db: Session, monkeypatch) -> None:
_user, space, account = seed(db)
document = StatementDocument(
space_id=space.id,
account_id=account.id,
original_filename="synthetic.pdf",
size_bytes=10,
content_sha256=uuid.uuid4().hex + uuid.uuid4().hex,
object_key=uuid.uuid4().hex,
dek_wrapped=b"key",
dek_wrap_nonce=b"nonce",
content_nonce=b"nonce",
status="queued",
)
db.add(document)
db.flush()
@contextmanager
def existing_session(_space_id):
yield db
class BrokenStore:
def get(self, _key):
raise RuntimeError("storage unavailable")
monkeypatch.setattr(service, "session_scope", existing_session)
monkeypatch.setattr(service, "get_blob_store", lambda: BrokenStore())
service.process_statement_document(document.id, space.id)
assert document.status == "failed"
assert document.error_code == "IMPORT_PARSE_FAILED"
def test_duplicate_candidate_can_reuse_unique_operation_without_client_id(db: Session) -> None:
user, space, first_document = ready_document(db)
service.confirm_document(db, space_id=space.id, document_id=first_document.id, user=user)
first_source = (
db.query(SourceRecord).filter_by(parse_run_id=first_document.active_parse_run_id).one()
)
second = StatementDocument(
space_id=space.id,
account_id=first_document.account_id,
original_filename="second.xlsx",
size_bytes=100,
content_sha256=uuid.uuid4().hex + uuid.uuid4().hex,
object_key=uuid.uuid4().hex,
status="review_required",
reconciliation={"full": True, "matched": True},
)
db.add(second)
db.flush()
run = ParseRun(
space_id=space.id,
document_id=second.id,
adapter_id="alfa_debit_xlsx",
status="succeeded",
)
db.add(run)
db.flush()
second.active_parse_run_id = run.id
duplicate_source = SourceRecord(
space_id=space.id,
document_id=second.id,
parse_run_id=run.id,
occurred_on=first_source.occurred_on,
amount_kopecks=first_source.amount_kopecks,
direction=first_source.direction,
row_ref="sheet0:row9",
record_fp=first_source.record_fp,
)
db.add(duplicate_source)
db.flush()
assert service._build_candidates(db, document=second, run=run)
candidate = db.query(DedupCandidate).filter_by(source_record_id=duplicate_source.id).one()
assert candidate.candidate_kind == "duplicate"
service.decide_source_record(
db,
space_id=space.id,
document_id=second.id,
source_record_id=duplicate_source.id,
decision="duplicate",
duplicate_operation_id=None,
user=user,
)
assert second.status == "ready"
service.confirm_document(db, space_id=space.id, document_id=second.id, user=user)
assert db.query(Operation).filter_by(space_id=space.id).count() == 1
def test_transfer_merge_revert_restores_counterpart_operation(db: Session) -> None:
user, space, first_document = ready_document(db)
service.confirm_document(db, space_id=space.id, document_id=first_document.id, user=user)
first_source = (
db.query(SourceRecord).filter_by(parse_run_id=first_document.active_parse_run_id).one()
)
target = create_account(
db,
space_id=space.id,
user=user,
name="Second debit",
kind="debit",
opened_on=date(2026, 1, 1),
)
second = StatementDocument(
space_id=space.id,
account_id=target.id,
original_filename="target.xlsx",
size_bytes=100,
content_sha256=uuid.uuid4().hex + uuid.uuid4().hex,
object_key=uuid.uuid4().hex,
status="review_required",
reconciliation={"full": True, "matched": True},
)
db.add(second)
db.flush()
run = ParseRun(
space_id=space.id,
document_id=second.id,
adapter_id="alfa_debit_xlsx",
status="succeeded",
)
db.add(run)
db.flush()
second.active_parse_run_id = run.id
incoming = SourceRecord(
space_id=space.id,
document_id=second.id,
parse_run_id=run.id,
occurred_on=first_source.occurred_on,
amount_kopecks=first_source.amount_kopecks,
direction="in",
row_ref="sheet0:row10",
record_fp=uuid.uuid4().hex,
)
db.add(incoming)
db.flush()
assert service._build_candidates(db, document=second, run=run)
candidate = db.query(DedupCandidate).filter_by(source_record_id=incoming.id).one()
assert candidate.candidate_kind == "transfer"
service.decide_source_record(
db,
space_id=space.id,
document_id=second.id,
source_record_id=incoming.id,
decision="transfer",
duplicate_operation_id=None,
user=user,
)
service.confirm_document(db, space_id=space.id, document_id=second.id, user=user)
assert db.query(Operation).filter_by(space_id=space.id, type="transfer").count() == 1
service.revert_document(db, space_id=space.id, document_id=second.id, user=user)
assert db.query(Operation).filter_by(space_id=space.id, type="transfer").count() == 0
assert db.query(Operation).filter_by(space_id=space.id, type="expense").count() == 1