2026-08-01 12:30:10 +05:30
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""Campaign status transitions must follow the approved state machine."""
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-08-01 12:56:09 +05:30
|
|
|
from adclaw.campaigns.models import CampaignSpec
|
|
|
|
|
from adclaw.campaigns.state import (
|
|
|
|
|
ALLOWED_TRANSITIONS,
|
|
|
|
|
EDITABLE_STATUSES,
|
|
|
|
|
CampaignStatus,
|
|
|
|
|
SyncStatus,
|
|
|
|
|
TransitionError,
|
|
|
|
|
next_status,
|
|
|
|
|
)
|
2026-08-01 12:30:10 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("current", "action", "expected"),
|
|
|
|
|
[
|
|
|
|
|
("draft", "submit", "pending_approval"),
|
|
|
|
|
("pending_approval", "approve", "approved"),
|
|
|
|
|
("pending_approval", "reject", "draft"),
|
|
|
|
|
("approved", "sync", "synced"),
|
|
|
|
|
("synced", "launch", "live"),
|
|
|
|
|
("paused", "launch", "live"),
|
|
|
|
|
("live", "pause", "paused"),
|
|
|
|
|
("live", "stop", "stopped"),
|
|
|
|
|
("paused", "stop", "stopped"),
|
|
|
|
|
("synced", "stop", "stopped"),
|
|
|
|
|
("stopped", "archive", "archived"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_allowed_transitions(current, action, expected):
|
|
|
|
|
assert next_status(current, action) == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("current", "action"),
|
|
|
|
|
[
|
|
|
|
|
("draft", "approve"),
|
|
|
|
|
("draft", "launch"),
|
|
|
|
|
("approved", "launch"),
|
|
|
|
|
("archived", "launch"),
|
|
|
|
|
("live", "approve"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_rejected_transitions(current, action):
|
|
|
|
|
with pytest.raises(TransitionError):
|
|
|
|
|
next_status(current, action)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_draft_cannot_reach_live_directly():
|
|
|
|
|
with pytest.raises(TransitionError):
|
|
|
|
|
next_status("draft", "launch")
|
2026-08-01 12:56:09 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unknown_status_raises():
|
|
|
|
|
with pytest.raises(TransitionError):
|
|
|
|
|
next_status("bogus", "submit")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_editable_statuses():
|
|
|
|
|
assert EDITABLE_STATUSES == frozenset({"draft", "pending_approval"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_allowed_transitions_keys_and_targets_are_valid_statuses():
|
|
|
|
|
valid_statuses = {status.value for status in CampaignStatus}
|
|
|
|
|
for current, actions in ALLOWED_TRANSITIONS.items():
|
|
|
|
|
assert current in valid_statuses
|
|
|
|
|
for target in actions.values():
|
|
|
|
|
assert target in valid_statuses
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_enum_string_equality():
|
|
|
|
|
assert CampaignStatus.DRAFT == "draft"
|
|
|
|
|
assert SyncStatus.NOT_SYNCED == "not_synced"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_campaign_spec_defaults():
|
|
|
|
|
spec = CampaignSpec(id="c1", name="n")
|
|
|
|
|
assert spec.status == "draft"
|
|
|
|
|
assert spec.origin == "maskanx"
|
|
|
|
|
assert spec.sync_status == "not_synced"
|
|
|
|
|
assert spec.budget == {}
|
|
|
|
|
assert spec.guardrails == {}
|
|
|
|
|
assert spec.targeting == {}
|
|
|
|
|
assert spec.advanced == {}
|
|
|
|
|
assert spec.schedule == {}
|
|
|
|
|
assert spec.channels == []
|