669 lines
27 KiB
Python
669 lines
27 KiB
Python
"""Tests for Project entity and related domain objects."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from noteflow.domain.entities.project import (
|
|
ExportRules,
|
|
Project,
|
|
ProjectSettings,
|
|
TriggerRules,
|
|
)
|
|
from noteflow.domain.errors import CannotArchiveDefaultProjectError, ValidationError
|
|
from noteflow.domain.utils.time import utc_now
|
|
from noteflow.domain.value_objects import ExportFormat
|
|
|
|
|
|
class TestExportRules:
|
|
"""Tests for ExportRules dataclass."""
|
|
|
|
def test_export_rules_default_values(self) -> None:
|
|
"""Test ExportRules has None defaults for all fields."""
|
|
rules = ExportRules()
|
|
assert rules.default_format is None, "default_format should default to None"
|
|
assert rules.include_audio is None, "include_audio should default to None"
|
|
assert rules.include_timestamps is None, "include_timestamps should default to None"
|
|
assert rules.template_id is None, "template_id should default to None"
|
|
|
|
def test_export_rules_with_values(self) -> None:
|
|
"""Test ExportRules accepts all field values."""
|
|
template_id = uuid4()
|
|
rules = ExportRules(
|
|
default_format=ExportFormat.PDF,
|
|
include_audio=True,
|
|
include_timestamps=False,
|
|
template_id=template_id,
|
|
)
|
|
assert rules.default_format == ExportFormat.PDF, "default_format should match"
|
|
assert rules.include_audio is True, "include_audio should match"
|
|
assert rules.include_timestamps is False, "include_timestamps should match"
|
|
assert rules.template_id == template_id, "template_id should match"
|
|
|
|
def test_export_rules_is_frozen(self) -> None:
|
|
"""Test ExportRules is immutable."""
|
|
from dataclasses import FrozenInstanceError
|
|
|
|
rules = ExportRules()
|
|
with pytest.raises(FrozenInstanceError, match="cannot assign"):
|
|
rules.default_format = ExportFormat.HTML
|
|
|
|
|
|
class TestTriggerRules:
|
|
"""Tests for TriggerRules dataclass."""
|
|
|
|
def test_trigger_rules_default_values(self) -> None:
|
|
"""Test TriggerRules has None defaults for all fields."""
|
|
rules = TriggerRules()
|
|
assert rules.auto_start_enabled is None, "auto_start_enabled should default to None"
|
|
assert rules.calendar_match_patterns is None, (
|
|
"calendar_match_patterns should default to None"
|
|
)
|
|
assert rules.app_match_patterns is None, "app_match_patterns should default to None"
|
|
|
|
def test_trigger_rules_with_values(self) -> None:
|
|
"""Test TriggerRules accepts all field values."""
|
|
rules = TriggerRules(
|
|
auto_start_enabled=True,
|
|
calendar_match_patterns=["*standup*", "*meeting*"],
|
|
app_match_patterns=["Zoom", "Teams"],
|
|
)
|
|
assert rules.auto_start_enabled is True, "auto_start_enabled should match"
|
|
assert rules.calendar_match_patterns == ["*standup*", "*meeting*"], (
|
|
"calendar_match_patterns should match"
|
|
)
|
|
assert rules.app_match_patterns == ["Zoom", "Teams"], "app_match_patterns should match"
|
|
|
|
def test_empty_list_clears_patterns(self) -> None:
|
|
"""Test empty list explicitly clears inherited patterns."""
|
|
rules = TriggerRules(
|
|
calendar_match_patterns=[],
|
|
app_match_patterns=[],
|
|
)
|
|
assert rules.calendar_match_patterns == [], "calendar_match_patterns should be empty list"
|
|
assert rules.app_match_patterns == [], "app_match_patterns should be empty list"
|
|
|
|
def test_trigger_rules_is_frozen(self) -> None:
|
|
"""Test TriggerRules is immutable."""
|
|
from dataclasses import FrozenInstanceError
|
|
|
|
rules = TriggerRules()
|
|
with pytest.raises(FrozenInstanceError, match="cannot assign"):
|
|
rules.auto_start_enabled = True
|
|
|
|
|
|
class TestProjectSettings:
|
|
"""Tests for ProjectSettings dataclass."""
|
|
|
|
def test_project_settings_default_values(self) -> None:
|
|
"""Test ProjectSettings has None defaults for all fields."""
|
|
settings = ProjectSettings()
|
|
assert settings.export_rules is None, "export_rules should default to None"
|
|
assert settings.trigger_rules is None, "trigger_rules should default to None"
|
|
assert settings.rag_enabled is None, "rag_enabled should default to None"
|
|
assert settings.default_summarization_template is None, (
|
|
"default_summarization_template should default to None"
|
|
)
|
|
|
|
def test_with_nested_rules(self) -> None:
|
|
"""Test ProjectSettings accepts nested rule objects."""
|
|
export_rules = ExportRules(default_format=ExportFormat.MARKDOWN)
|
|
trigger_rules = TriggerRules(auto_start_enabled=True)
|
|
settings = ProjectSettings(
|
|
export_rules=export_rules,
|
|
trigger_rules=trigger_rules,
|
|
rag_enabled=True,
|
|
default_summarization_template="professional",
|
|
)
|
|
assert settings.export_rules == export_rules, "export_rules should match"
|
|
assert settings.trigger_rules == trigger_rules, "trigger_rules should match"
|
|
assert settings.rag_enabled is True, "rag_enabled should match"
|
|
assert settings.default_summarization_template == "professional", "template should match"
|
|
|
|
|
|
class TestProjectCreation:
|
|
"""Tests for Project creation and initialization."""
|
|
|
|
def test_create_with_required_fields(self) -> None:
|
|
"""Test Project creation with only required fields."""
|
|
project_id = uuid4()
|
|
workspace_id = uuid4()
|
|
project = Project(
|
|
id=project_id,
|
|
workspace_id=workspace_id,
|
|
name="Test Project",
|
|
)
|
|
assert project.id == project_id, "id should match"
|
|
assert project.workspace_id == workspace_id, "workspace_id should match"
|
|
assert project.name == "Test Project", "name should match"
|
|
|
|
@pytest.mark.parametrize(
|
|
("attr", "expected"),
|
|
[
|
|
("slug", None),
|
|
("description", None),
|
|
("is_default", False),
|
|
("archived_at", None),
|
|
],
|
|
)
|
|
def test_default_attribute_values(self, attr: str, expected: object) -> None:
|
|
"""Test Project has expected default values."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
actual = getattr(project, attr)
|
|
assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"
|
|
|
|
def test_default_settings_factory(self) -> None:
|
|
"""Test Project creates empty ProjectSettings by default."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert isinstance(project.settings, ProjectSettings), "settings should be ProjectSettings"
|
|
assert project.settings.export_rules is None, "export_rules should default to None"
|
|
assert project.settings.trigger_rules is None, "trigger_rules should default to None"
|
|
|
|
def test_default_metadata_factory(self) -> None:
|
|
"""Test Project creates empty metadata dict by default."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.metadata == {}, "metadata should default to empty dict"
|
|
assert isinstance(project.metadata, dict), "metadata should be a dict"
|
|
|
|
def test_timestamps_are_set(self) -> None:
|
|
"""Test created_at and updated_at are set on creation."""
|
|
before = utc_now()
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
after = utc_now()
|
|
|
|
assert project.created_at is not None, "created_at should be set"
|
|
assert project.updated_at is not None, "updated_at should be set"
|
|
assert before <= project.created_at <= after, "created_at should be within test window"
|
|
assert before <= project.updated_at <= after, "updated_at should be within test window"
|
|
|
|
|
|
class TestProjectSlugValidation:
|
|
"""Tests for Project slug validation."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"valid_slug",
|
|
[
|
|
"my-project",
|
|
"project123",
|
|
"a",
|
|
"test-project-2024",
|
|
"123",
|
|
"a-b-c",
|
|
],
|
|
)
|
|
def test_valid_slug_patterns(self, valid_slug: str) -> None:
|
|
"""Test valid slug patterns are accepted."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
slug=valid_slug,
|
|
)
|
|
assert project.slug == valid_slug, f"expected slug={valid_slug!r}, got {project.slug!r}"
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_slug",
|
|
[
|
|
"My-Project", # uppercase
|
|
"my_project", # underscore
|
|
"my project", # space
|
|
"project!", # special char
|
|
"UPPERCASE", # all uppercase
|
|
"Project", # mixed case
|
|
"", # empty string
|
|
],
|
|
)
|
|
def test_invalid_slug_patterns_raise(self, invalid_slug: str) -> None:
|
|
"""Test invalid slug patterns raise ValidationError."""
|
|
with pytest.raises(ValidationError, match="slug"):
|
|
Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
slug=invalid_slug,
|
|
)
|
|
|
|
def test_none_slug_allowed(self) -> None:
|
|
"""Test None slug is allowed (no validation)."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
slug=None,
|
|
)
|
|
assert project.slug is None, f"expected slug=None, got {project.slug!r}"
|
|
|
|
|
|
class TestProjectArchiveRestore:
|
|
"""Tests for Project archive and restore operations."""
|
|
|
|
def test_archive_sets_timestamp(self) -> None:
|
|
"""Test archive() sets archived_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.archived_at is None, "archived_at should be None before archive"
|
|
|
|
before = utc_now()
|
|
project.archive()
|
|
after = utc_now()
|
|
|
|
assert project.archived_at is not None, "archived_at should be set"
|
|
assert before <= project.archived_at <= after, "archived_at should be within test window"
|
|
|
|
def test_archive_updates_timestamp(self) -> None:
|
|
"""Test archive() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_updated = project.updated_at
|
|
|
|
project.archive()
|
|
|
|
assert project.updated_at >= original_updated, (
|
|
"updated_at should be >= original after archive"
|
|
)
|
|
|
|
def test_archive_default_project_raises(self) -> None:
|
|
"""Test archiving default project raises CannotArchiveDefaultProjectError."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Default",
|
|
is_default=True,
|
|
)
|
|
with pytest.raises(CannotArchiveDefaultProjectError, match="Cannot archive"):
|
|
project.archive()
|
|
|
|
def test_restore_clears_archived_at(self) -> None:
|
|
"""Test restore() clears archived_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.archive()
|
|
assert project.archived_at is not None, "archived_at should be set after archive"
|
|
|
|
project.restore()
|
|
|
|
assert project.archived_at is None, "archived_at should be cleared"
|
|
|
|
def test_restore_updates_timestamp(self) -> None:
|
|
"""Test restore() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.archive()
|
|
archived_updated = project.updated_at
|
|
|
|
project.restore()
|
|
|
|
assert project.updated_at >= archived_updated, (
|
|
"updated_at should be >= archived_updated after restore"
|
|
)
|
|
|
|
|
|
class TestProjectProperties:
|
|
"""Tests for Project computed properties."""
|
|
|
|
def test_is_archived_false_by_default(self) -> None:
|
|
"""Test is_archived returns False for new project."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.is_archived is False, "is_archived should be False for new project"
|
|
|
|
def test_is_active_true_by_default(self) -> None:
|
|
"""Test is_active returns True for new project."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.is_active is True, "is_active should be True for new project"
|
|
|
|
@pytest.mark.parametrize(
|
|
("attribute", "expected"),
|
|
[
|
|
pytest.param("is_archived", True, id="archived_true"),
|
|
pytest.param("is_active", False, id="active_false"),
|
|
],
|
|
)
|
|
def test_project_properties_when_archived(self, attribute: str, expected: bool) -> None:
|
|
"""Test archived project properties."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.archive()
|
|
assert getattr(project, attribute) is expected
|
|
|
|
def test_is_archived_and_is_active_are_inverse(self) -> None:
|
|
"""Test is_archived and is_active are always inverse."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.is_archived != project.is_active, "should be inverse when new"
|
|
|
|
project.archive()
|
|
assert project.is_archived != project.is_active, "should be inverse when archived"
|
|
|
|
project.restore()
|
|
assert project.is_archived != project.is_active, "should be inverse when restored"
|
|
|
|
|
|
class TestProjectMutations:
|
|
"""Tests for Project mutation methods."""
|
|
|
|
def test_update_name(self) -> None:
|
|
"""Test update_name() changes name field."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Original")
|
|
project.update_name("Updated Name")
|
|
assert project.name == "Updated Name", f"expected name='Updated Name', got {project.name!r}"
|
|
|
|
def test_update_name_updates_timestamp(self) -> None:
|
|
"""Test update_name() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Original")
|
|
original_updated = project.updated_at
|
|
|
|
project.update_name("Updated")
|
|
|
|
assert project.updated_at >= original_updated, (
|
|
"updated_at should be >= original after update_name"
|
|
)
|
|
|
|
def test_update_description(self) -> None:
|
|
"""Test update_description() changes description field."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.update_description("New description")
|
|
assert project.description == "New description", (
|
|
f"expected description='New description', got {project.description!r}"
|
|
)
|
|
|
|
def test_update_description_to_none(self) -> None:
|
|
"""Test update_description() can set description to None."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
description="Original",
|
|
)
|
|
project.update_description(None)
|
|
assert project.description is None, (
|
|
f"expected description=None, got {project.description!r}"
|
|
)
|
|
|
|
def test_update_description_updates_timestamp(self) -> None:
|
|
"""Test update_description() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_updated = project.updated_at
|
|
|
|
project.update_description("Description")
|
|
|
|
assert project.updated_at >= original_updated, (
|
|
"updated_at should be >= original after update_description"
|
|
)
|
|
|
|
def test_update_settings(self) -> None:
|
|
"""Test update_settings() replaces settings object."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
new_settings = ProjectSettings(rag_enabled=True)
|
|
|
|
project.update_settings(new_settings)
|
|
|
|
assert project.settings == new_settings, "settings should match new settings"
|
|
assert project.settings.rag_enabled is True, "rag_enabled should be True"
|
|
|
|
def test_update_settings_updates_timestamp(self) -> None:
|
|
"""Test update_settings() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_updated = project.updated_at
|
|
|
|
project.update_settings(ProjectSettings())
|
|
|
|
assert project.updated_at >= original_updated, (
|
|
"updated_at should be >= original after update_settings"
|
|
)
|
|
|
|
def test_update_slug_with_valid_pattern(self) -> None:
|
|
"""Test update_slug() accepts valid slug patterns."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.update_slug("new-valid-slug")
|
|
assert project.slug == "new-valid-slug", "Slug should be updated"
|
|
|
|
def test_update_slug_with_invalid_pattern_raises(self) -> None:
|
|
"""Test update_slug() raises ValidationError for invalid patterns."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
with pytest.raises(ValidationError, match="slug"):
|
|
project.update_slug("Invalid-Slug")
|
|
|
|
def test_update_slug_to_none_allowed(self) -> None:
|
|
"""Test update_slug() can set slug to None."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Test",
|
|
slug="original-slug",
|
|
)
|
|
project.update_slug(None)
|
|
assert project.slug is None, "Slug should be cleared to None"
|
|
|
|
def test_update_slug_updates_timestamp(self) -> None:
|
|
"""Test update_slug() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_updated = project.updated_at
|
|
|
|
project.update_slug("new-slug")
|
|
|
|
assert project.updated_at >= original_updated, "updated_at should be updated"
|
|
|
|
def test_set_metadata(self) -> None:
|
|
"""Test set_metadata() adds key-value pair."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.set_metadata("key", "value")
|
|
assert project.metadata["key"] == "value", (
|
|
f"expected metadata['key']='value', got {project.metadata.get('key')!r}"
|
|
)
|
|
|
|
def test_set_metadata_overwrites_existing(self) -> None:
|
|
"""Test set_metadata() overwrites existing key."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.set_metadata("key", "original")
|
|
project.set_metadata("key", "updated")
|
|
assert project.metadata["key"] == "updated", (
|
|
f"expected metadata['key']='updated', got {project.metadata.get('key')!r}"
|
|
)
|
|
|
|
def test_set_metadata_updates_timestamp(self) -> None:
|
|
"""Test set_metadata() updates updated_at timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_updated = project.updated_at
|
|
|
|
project.set_metadata("key", "value")
|
|
|
|
assert project.updated_at >= original_updated, (
|
|
"updated_at should be >= original after set_metadata"
|
|
)
|
|
|
|
|
|
class TestProjectEdgeCases:
|
|
"""Edge case tests for Project entity."""
|
|
|
|
def test_archive_already_archived_project(self) -> None:
|
|
"""Test archiving already archived project updates timestamp."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
project.archive()
|
|
first_archived = project.archived_at
|
|
assert first_archived is not None, "first_archived should be set"
|
|
|
|
# Small delay to ensure different timestamp
|
|
project.archive()
|
|
|
|
assert project.archived_at is not None, (
|
|
"archived_at should still be set after second archive"
|
|
)
|
|
assert project.archived_at >= first_archived, (
|
|
"archived_at should be >= first_archived after second archive"
|
|
)
|
|
|
|
def test_restore_non_archived_project(self) -> None:
|
|
"""Test restoring non-archived project is idempotent."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.archived_at is None, "archived_at should be None before restore"
|
|
|
|
project.restore()
|
|
|
|
assert project.archived_at is None, (
|
|
"archived_at should remain None after restore on non-archived project"
|
|
)
|
|
|
|
def test_metadata_complex_structures(self) -> None:
|
|
"""Test metadata can store complex nested structures."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
complex_value = {
|
|
"nested": {"key": "value"},
|
|
"list": [1, 2, 3],
|
|
"boolean": True,
|
|
}
|
|
project.set_metadata("complex", complex_value)
|
|
assert project.metadata["complex"] == complex_value, (
|
|
"complex nested structure should be stored in metadata"
|
|
)
|
|
|
|
def test_very_long_name(self) -> None:
|
|
"""Test project accepts very long names."""
|
|
long_name = "A" * 1000
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name=long_name)
|
|
assert len(project.name) == 1000, f"expected name length 1000, got {len(project.name)}"
|
|
|
|
def test_unicode_name_and_description(self) -> None:
|
|
"""Test project accepts unicode in name and description."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="项目名称", # Chinese for "project name"
|
|
description="プロジェクトの説明", # Japanese for "project description"
|
|
)
|
|
assert project.name == "项目名称", f"expected Chinese name, got {project.name!r}"
|
|
assert project.description == "プロジェクトの説明", (
|
|
f"expected Japanese description, got {project.description!r}"
|
|
)
|
|
|
|
def test_emoji_in_name(self) -> None:
|
|
"""Test project accepts emoji in name."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="My Project 🚀",
|
|
)
|
|
assert "🚀" in project.name, f"expected emoji in name, got {project.name!r}"
|
|
|
|
def test_settings_with_full_configuration(self) -> None:
|
|
"""Test project with fully configured settings."""
|
|
template_id = uuid4()
|
|
settings = ProjectSettings(
|
|
export_rules=ExportRules(
|
|
default_format=ExportFormat.PDF,
|
|
include_audio=True,
|
|
include_timestamps=True,
|
|
template_id=template_id,
|
|
),
|
|
trigger_rules=TriggerRules(
|
|
auto_start_enabled=True,
|
|
calendar_match_patterns=["*standup*"],
|
|
app_match_patterns=["Zoom"],
|
|
),
|
|
rag_enabled=True,
|
|
default_summarization_template="professional",
|
|
)
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Fully Configured",
|
|
settings=settings,
|
|
)
|
|
assert project.settings.export_rules is not None, "export_rules should not be None"
|
|
assert project.settings.export_rules.default_format == ExportFormat.PDF, (
|
|
"default_format should be PDF"
|
|
)
|
|
assert project.settings.trigger_rules is not None, "trigger_rules should not be None"
|
|
assert project.settings.trigger_rules.auto_start_enabled is True, (
|
|
"auto_start_enabled should be True"
|
|
)
|
|
|
|
|
|
class TestProjectInvariants:
|
|
"""Tests for Project domain invariants."""
|
|
|
|
def test_default_project_cannot_be_archived(self) -> None:
|
|
"""Test default project always rejects archive operation."""
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Default Project",
|
|
is_default=True,
|
|
)
|
|
# Multiple attempts should all fail
|
|
with pytest.raises(CannotArchiveDefaultProjectError, match="Cannot archive"):
|
|
project.archive()
|
|
|
|
# Project state should be unchanged
|
|
assert project.archived_at is None, "archived_at should remain None for default project"
|
|
assert project.is_active is True, "is_active should remain True for default project"
|
|
|
|
def test_updated_at_never_before_created_at(self) -> None:
|
|
"""Test updated_at is always >= created_at."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
assert project.updated_at >= project.created_at, (
|
|
"updated_at should be >= created_at on creation"
|
|
)
|
|
|
|
project.update_name("New Name")
|
|
assert project.updated_at >= project.created_at, (
|
|
"updated_at should be >= created_at after update_name"
|
|
)
|
|
|
|
project.archive()
|
|
assert project.updated_at >= project.created_at, (
|
|
"updated_at should be >= created_at after archive"
|
|
)
|
|
|
|
def test_explicit_timestamps_preserved(self) -> None:
|
|
"""Test explicit timestamps are preserved on creation."""
|
|
created = utc_now() - timedelta(days=30)
|
|
updated = utc_now() - timedelta(days=15)
|
|
|
|
project = Project(
|
|
id=uuid4(),
|
|
workspace_id=uuid4(),
|
|
name="Historical",
|
|
created_at=created,
|
|
updated_at=updated,
|
|
)
|
|
assert project.created_at == created, (
|
|
f"expected created_at={created}, got {project.created_at}"
|
|
)
|
|
assert project.updated_at == updated, (
|
|
f"expected updated_at={updated}, got {project.updated_at}"
|
|
)
|
|
|
|
def test_id_immutability(self) -> None:
|
|
"""Test project id cannot be changed after creation."""
|
|
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
|
|
original_id = project.id
|
|
|
|
# Dataclass allows reassignment but domain logic should prevent it
|
|
# This documents current behavior - consider making id immutable
|
|
project.id = uuid4()
|
|
|
|
# Just verifying the field was changed (dataclass allows this)
|
|
assert project.id != original_id, (
|
|
"id should have been changed (dataclass allows reassignment)"
|
|
)
|
|
|
|
|
|
class TestCannotArchiveDefaultProjectError:
|
|
"""Tests for CannotArchiveDefaultProjectError."""
|
|
|
|
def test_error_message_includes_project_id(self) -> None:
|
|
"""Test error message includes the project ID."""
|
|
project_id = "test-project-123"
|
|
error = CannotArchiveDefaultProjectError(project_id)
|
|
# Use structured details instead of string comparison
|
|
assert error.details is not None, "error.details should not be None"
|
|
assert error.details.get("project_id") == project_id, (
|
|
f"expected project_id='{project_id}' in details"
|
|
)
|
|
|
|
def test_error_details_contain_project_id(self) -> None:
|
|
"""Test error details contain project_id key."""
|
|
project_id = "test-project-456"
|
|
error = CannotArchiveDefaultProjectError(project_id)
|
|
assert error.details is not None, "error.details should not be None"
|
|
assert error.details.get("project_id") == project_id, (
|
|
f"expected project_id='{project_id}' in details, got {error.details.get('project_id')!r}"
|
|
)
|