Files
roboco/tests/unit/models/test_budget_fields.py
T

166 lines
5.2 KiB
Python

"""Task.budget_usd / Project.monthly_budget_usd validation (#654).
The task-budgets feature's own design says "0 rejected — a zero budget
silently blocks everything" (every claim is refused from the first tick),
so every schema that can set these fields must reject 0 and negative values
at the pydantic boundary — a 422, never a stored self-DoS. Null ("no cap")
stays valid throughout. Mirrors test_project_sandbox_services.py's style
(domain-model `pytest.raises(ValidationError)` coverage).
"""
from __future__ import annotations
from uuid import uuid4
import pytest
from pydantic import ValidationError
from roboco.models.base import Team
from roboco.models.project import Project, ProjectCreate, ProjectUpdate
from roboco.models.task import Task, TaskUpdate
def _task(budget_usd: float | None = None) -> Task:
return Task(
title="Add user lookup endpoint",
description="Add GET /v1/users/{id} returning user JSON.",
acceptance_criteria=["returns 404 for unknown user"],
created_by=uuid4(),
team=Team.BACKEND,
budget_usd=budget_usd,
)
def _project(monthly_budget_usd: float | None = None) -> Project:
return Project(
name="P",
slug="p",
git_url="https://example.com/r.git",
assigned_cell=Team.BACKEND,
created_by=uuid4(),
monthly_budget_usd=monthly_budget_usd,
)
# ---------------------------------------------------------------------------
# Task.budget_usd
# ---------------------------------------------------------------------------
def test_task_defaults_budget_usd_to_none() -> None:
assert _task().budget_usd is None
def test_task_accepts_positive_budget_usd() -> None:
budget = 12.5
assert _task(budget_usd=budget).budget_usd == budget
def test_task_rejects_zero_budget_usd() -> None:
with pytest.raises(ValidationError, match="budget_usd"):
_task(budget_usd=0)
def test_task_rejects_negative_budget_usd() -> None:
with pytest.raises(ValidationError, match="budget_usd"):
_task(budget_usd=-5)
# ---------------------------------------------------------------------------
# roboco.models.task.TaskUpdate.budget_usd (domain update model)
# ---------------------------------------------------------------------------
def test_task_update_accepts_null_budget_usd() -> None:
assert TaskUpdate(budget_usd=None).budget_usd is None
def test_task_update_accepts_positive_budget_usd() -> None:
budget = 3.0
assert TaskUpdate(budget_usd=budget).budget_usd == budget
def test_task_update_rejects_zero_budget_usd() -> None:
with pytest.raises(ValidationError, match="budget_usd"):
TaskUpdate(budget_usd=0)
def test_task_update_rejects_negative_budget_usd() -> None:
with pytest.raises(ValidationError, match="budget_usd"):
TaskUpdate(budget_usd=-1)
# ---------------------------------------------------------------------------
# Project.monthly_budget_usd
# ---------------------------------------------------------------------------
def test_project_defaults_monthly_budget_usd_to_none() -> None:
assert _project().monthly_budget_usd is None
def test_project_accepts_positive_monthly_budget_usd() -> None:
cap = 100.0
assert _project(monthly_budget_usd=cap).monthly_budget_usd == cap
def test_project_rejects_zero_monthly_budget_usd() -> None:
with pytest.raises(ValidationError, match="monthly_budget_usd"):
_project(monthly_budget_usd=0)
def test_project_rejects_negative_monthly_budget_usd() -> None:
with pytest.raises(ValidationError, match="monthly_budget_usd"):
_project(monthly_budget_usd=-5)
# ---------------------------------------------------------------------------
# ProjectCreate.monthly_budget_usd
# ---------------------------------------------------------------------------
def test_project_create_accepts_null_monthly_budget_usd() -> None:
assert (
ProjectCreate(
name="P",
slug="p",
git_url="https://example.com/r.git",
assigned_cell=Team.BACKEND,
).monthly_budget_usd
is None
)
def test_project_create_rejects_zero_monthly_budget_usd() -> None:
with pytest.raises(ValidationError, match="monthly_budget_usd"):
ProjectCreate(
name="P",
slug="p",
git_url="https://example.com/r.git",
assigned_cell=Team.BACKEND,
monthly_budget_usd=0,
)
# ---------------------------------------------------------------------------
# ProjectUpdate.monthly_budget_usd
# ---------------------------------------------------------------------------
def test_project_update_accepts_null_monthly_budget_usd() -> None:
assert ProjectUpdate(monthly_budget_usd=None).monthly_budget_usd is None
def test_project_update_accepts_positive_monthly_budget_usd() -> None:
cap = 50.0
assert ProjectUpdate(monthly_budget_usd=cap).monthly_budget_usd == cap
def test_project_update_rejects_zero_monthly_budget_usd() -> None:
with pytest.raises(ValidationError, match="monthly_budget_usd"):
ProjectUpdate(monthly_budget_usd=0)
def test_project_update_rejects_negative_monthly_budget_usd() -> None:
with pytest.raises(ValidationError, match="monthly_budget_usd"):
ProjectUpdate(monthly_budget_usd=-5)