d582f15681
- 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.
156 lines
4.1 KiB
Python
156 lines
4.1 KiB
Python
from datetime import date
|
|
|
|
import pytest
|
|
from hypothesis import HealthCheck, given, settings
|
|
from hypothesis import strategies as st
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.errors import AppError
|
|
from app.identity.models import User
|
|
from app.ledger.models import Posting
|
|
from app.ledger.service import (
|
|
account_balance,
|
|
create_account,
|
|
create_expense,
|
|
create_income,
|
|
create_refund,
|
|
create_transfer,
|
|
get_account,
|
|
)
|
|
from app.spaces.service import create_space
|
|
|
|
|
|
def seed(db: Session):
|
|
user = User(email="owner@example.test", password_hash="unused-in-domain-tests")
|
|
db.add(user)
|
|
db.flush()
|
|
space = create_space(db, user=user, name="Personal")
|
|
return user, space
|
|
|
|
|
|
@given(amount=st.integers(min_value=1, max_value=10**12))
|
|
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
def test_transfer_postings_conserve_money(db: Session, amount: int) -> None:
|
|
db.rollback()
|
|
user, space = seed(db)
|
|
source = create_account(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
name="Debit",
|
|
kind="debit",
|
|
opened_on=date(2026, 1, 1),
|
|
opening_amount_kopecks=amount,
|
|
)
|
|
target = create_account(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
name="Cash",
|
|
kind="cash",
|
|
opened_on=date(2026, 1, 1),
|
|
)
|
|
operation = create_transfer(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
source_account_id=source.id,
|
|
target_account_id=target.id,
|
|
amount_kopecks=amount,
|
|
occurred_on=date(2026, 1, 2),
|
|
)
|
|
db.flush()
|
|
postings = db.query(Posting).filter_by(operation_id=operation.id).all()
|
|
assert len(postings) == 2
|
|
assert sum(item.amount_kopecks for item in postings) == 0
|
|
assert account_balance(db, source) == 0
|
|
assert account_balance(db, target) == amount
|
|
db.rollback()
|
|
|
|
|
|
def test_credit_expense_is_negative_balance_and_refund_is_capped(db: Session) -> None:
|
|
user, space = seed(db)
|
|
card = create_account(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
name="Credit",
|
|
kind="credit",
|
|
opened_on=date(2026, 2, 1),
|
|
)
|
|
expense = create_expense(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
account_id=card.id,
|
|
amount_kopecks=12_345,
|
|
occurred_on=date(2026, 2, 2),
|
|
)
|
|
assert account_balance(db, card) == -12_345
|
|
create_refund(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
parent_operation_id=expense.id,
|
|
amount_kopecks=2_345,
|
|
occurred_on=date(2026, 2, 3),
|
|
)
|
|
assert account_balance(db, card) == -10_000
|
|
with pytest.raises(AppError, match="Refund exceeds"):
|
|
create_refund(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
parent_operation_id=expense.id,
|
|
amount_kopecks=10_001,
|
|
occurred_on=date(2026, 2, 4),
|
|
)
|
|
|
|
|
|
def test_cross_space_lookup_fails_closed(db: Session) -> None:
|
|
user, first = seed(db)
|
|
other = create_space(db, user=user, name="Other")
|
|
account = create_account(
|
|
db,
|
|
space_id=first.id,
|
|
user=user,
|
|
name="Cash",
|
|
kind="cash",
|
|
opened_on=date(2026, 1, 1),
|
|
)
|
|
with pytest.raises(AppError) as exc:
|
|
get_account(db, space_id=other.id, account_id=account.id)
|
|
assert exc.value.code == "LEDGER_ACCOUNT_NOT_FOUND"
|
|
|
|
|
|
def test_idempotency_key_prevents_duplicate_income(db: Session) -> None:
|
|
user, space = seed(db)
|
|
account = create_account(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
name="Debit",
|
|
kind="debit",
|
|
opened_on=date(2026, 1, 1),
|
|
)
|
|
first = create_income(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
account_id=account.id,
|
|
amount_kopecks=50_000,
|
|
occurred_on=date(2026, 1, 5),
|
|
request_key="retry-1",
|
|
)
|
|
second = create_income(
|
|
db,
|
|
space_id=space.id,
|
|
user=user,
|
|
account_id=account.id,
|
|
amount_kopecks=50_000,
|
|
occurred_on=date(2026, 1, 5),
|
|
request_key="retry-1",
|
|
)
|
|
assert first.id == second.id
|
|
assert account_balance(db, account) == 50_000
|