feat(hosting,prospecting): add hosting unit tests and fix template bugs
All checks were successful
All checks were successful
- Add 55 unit tests for hosting module (hosted site service, client service service, stats service) with full fixture setup - Fix table_empty_state macro: add x_message param for dynamic Alpine.js expressions rendered via x-text instead of server-side Jinja - Fix hosting templates (sites, clients) using message= with Alpine expressions that rendered as literal text - Fix prospecting templates (leads, scan-jobs, prospects) using nonexistent subtitle= param, migrated to x_message= - Align hosting and prospecting admin templates with shared design system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
app/modules/hosting/tests/unit/__init__.py
Normal file
0
app/modules/hosting/tests/unit/__init__.py
Normal file
219
app/modules/hosting/tests/unit/test_client_service_service.py
Normal file
219
app/modules/hosting/tests/unit/test_client_service_service.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# app/modules/hosting/tests/unit/test_client_service_service.py
|
||||
"""
|
||||
Unit tests for ClientServiceService.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.hosting.exceptions import ClientServiceNotFoundException
|
||||
from app.modules.hosting.models import ClientServiceStatus, ServiceType
|
||||
from app.modules.hosting.services.client_service_service import ClientServiceService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestClientServiceService:
|
||||
"""Tests for ClientServiceService CRUD operations."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = ClientServiceService()
|
||||
|
||||
def test_create_domain_service(self, db, hosted_site):
|
||||
"""Test creating a domain service."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{
|
||||
"service_type": "domain",
|
||||
"name": "example.lu",
|
||||
"domain_name": "example.lu",
|
||||
"registrar": "Gandi",
|
||||
"billing_period": "annual",
|
||||
"price_cents": 1500,
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.id is not None
|
||||
assert svc.service_type == ServiceType.DOMAIN
|
||||
assert svc.status == ClientServiceStatus.PENDING
|
||||
assert svc.domain_name == "example.lu"
|
||||
assert svc.registrar == "Gandi"
|
||||
assert svc.price_cents == 1500
|
||||
|
||||
def test_create_email_service(self, db, hosted_site):
|
||||
"""Test creating an email service."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{
|
||||
"service_type": "email",
|
||||
"name": "Email Hosting",
|
||||
"mailbox_count": 10,
|
||||
"billing_period": "monthly",
|
||||
"price_cents": 900,
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.service_type == ServiceType.EMAIL
|
||||
assert svc.mailbox_count == 10
|
||||
|
||||
def test_create_ssl_service(self, db, hosted_site):
|
||||
"""Test creating an SSL service."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{
|
||||
"service_type": "ssl",
|
||||
"name": "Let's Encrypt SSL",
|
||||
"price_cents": 0,
|
||||
"expires_at": datetime.now(UTC) + timedelta(days=90),
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.service_type == ServiceType.SSL
|
||||
assert svc.expires_at is not None
|
||||
|
||||
def test_create_hosting_service(self, db, hosted_site):
|
||||
"""Test creating a hosting service."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{
|
||||
"service_type": "hosting",
|
||||
"name": "Shared Hosting",
|
||||
"billing_period": "monthly",
|
||||
"price_cents": 2000,
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.service_type == ServiceType.HOSTING
|
||||
|
||||
def test_create_maintenance_service(self, db, hosted_site):
|
||||
"""Test creating a website maintenance service."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{
|
||||
"service_type": "website_maintenance",
|
||||
"name": "Monthly Maintenance",
|
||||
"billing_period": "monthly",
|
||||
"price_cents": 5000,
|
||||
"notes": "Includes security updates",
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.service_type == ServiceType.WEBSITE_MAINTENANCE
|
||||
assert svc.notes == "Includes security updates"
|
||||
|
||||
def test_get_by_id(self, db, client_service_domain):
|
||||
"""Test getting a service by ID."""
|
||||
result = self.service.get_by_id(db, client_service_domain.id)
|
||||
assert result.id == client_service_domain.id
|
||||
|
||||
def test_get_by_id_not_found(self, db):
|
||||
"""Test getting non-existent service raises exception."""
|
||||
with pytest.raises(ClientServiceNotFoundException):
|
||||
self.service.get_by_id(db, 99999)
|
||||
|
||||
def test_get_for_site(self, db, hosted_site, client_service_domain, client_service_email):
|
||||
"""Test getting all services for a site."""
|
||||
services = self.service.get_for_site(db, hosted_site.id)
|
||||
assert len(services) >= 2
|
||||
|
||||
def test_update_service(self, db, client_service_domain):
|
||||
"""Test updating a service."""
|
||||
updated = self.service.update(
|
||||
db,
|
||||
client_service_domain.id,
|
||||
{"name": "updated-domain.lu", "price_cents": 3000},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert updated.name == "updated-domain.lu"
|
||||
assert updated.price_cents == 3000
|
||||
|
||||
def test_update_service_status(self, db, client_service_domain):
|
||||
"""Test updating a service status."""
|
||||
updated = self.service.update(
|
||||
db,
|
||||
client_service_domain.id,
|
||||
{"status": ClientServiceStatus.SUSPENDED},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert updated.status == ClientServiceStatus.SUSPENDED
|
||||
|
||||
def test_delete_service(self, db, client_service_domain):
|
||||
"""Test deleting a service."""
|
||||
svc_id = client_service_domain.id
|
||||
self.service.delete(db, svc_id)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(ClientServiceNotFoundException):
|
||||
self.service.get_by_id(db, svc_id)
|
||||
|
||||
def test_get_expiring_soon(self, db, client_service_expiring):
|
||||
"""Test getting services expiring within 30 days."""
|
||||
expiring = self.service.get_expiring_soon(db, days=30)
|
||||
assert len(expiring) >= 1
|
||||
assert all(s.expires_at is not None for s in expiring)
|
||||
|
||||
def test_get_expiring_soon_excludes_distant(self, db, client_service_domain):
|
||||
"""Test that services expiring far in the future are excluded."""
|
||||
# client_service_domain expires in 365 days
|
||||
expiring = self.service.get_expiring_soon(db, days=30)
|
||||
ids = [s.id for s in expiring]
|
||||
assert client_service_domain.id not in ids
|
||||
|
||||
def test_get_all(self, db, client_service_domain, client_service_email):
|
||||
"""Test listing all services with pagination."""
|
||||
services, total = self.service.get_all(db)
|
||||
assert total >= 2
|
||||
assert len(services) >= 2
|
||||
|
||||
def test_get_all_filter_type(self, db, client_service_domain, client_service_email):
|
||||
"""Test filtering services by type."""
|
||||
services, total = self.service.get_all(db, service_type="domain")
|
||||
assert total >= 1
|
||||
assert all(s.service_type == ServiceType.DOMAIN for s in services)
|
||||
|
||||
def test_get_all_filter_status(self, db, client_service_domain):
|
||||
"""Test filtering services by status."""
|
||||
services, total = self.service.get_all(db, status="active")
|
||||
assert total >= 1
|
||||
assert all(s.status == ClientServiceStatus.ACTIVE for s in services)
|
||||
|
||||
def test_get_all_pagination(self, db, client_service_domain, client_service_email):
|
||||
"""Test pagination."""
|
||||
services, total = self.service.get_all(db, page=1, per_page=1)
|
||||
assert len(services) == 1
|
||||
assert total >= 2
|
||||
|
||||
def test_default_currency(self, db, hosted_site):
|
||||
"""Test that default currency is EUR."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{"service_type": "hosting", "name": "Test"},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.currency == "EUR"
|
||||
|
||||
def test_auto_renew_default(self, db, hosted_site):
|
||||
"""Test that auto_renew defaults to True."""
|
||||
svc = self.service.create(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{"service_type": "hosting", "name": "Test"},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert svc.auto_renew is True
|
||||
325
app/modules/hosting/tests/unit/test_hosted_site_service.py
Normal file
325
app/modules/hosting/tests/unit/test_hosted_site_service.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# app/modules/hosting/tests/unit/test_hosted_site_service.py
|
||||
"""
|
||||
Unit tests for HostedSiteService.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.hosting.exceptions import (
|
||||
HostedSiteNotFoundException,
|
||||
InvalidStatusTransitionException,
|
||||
)
|
||||
from app.modules.hosting.models import HostedSiteStatus
|
||||
from app.modules.hosting.services.hosted_site_service import (
|
||||
HostedSiteService,
|
||||
_slugify,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestSlugify:
|
||||
"""Tests for the _slugify helper."""
|
||||
|
||||
def test_basic_slug(self):
|
||||
assert _slugify("My Business") == "my-business"
|
||||
|
||||
def test_special_characters_removed(self):
|
||||
assert _slugify("Café & Brasserie!") == "caf-brasserie"
|
||||
|
||||
def test_multiple_spaces_collapsed(self):
|
||||
assert _slugify(" Too Many Spaces ") == "too-many-spaces"
|
||||
|
||||
def test_max_length(self):
|
||||
long_name = "A" * 100
|
||||
assert len(_slugify(long_name)) <= 50
|
||||
|
||||
def test_empty_string(self):
|
||||
assert _slugify("") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestHostedSiteService:
|
||||
"""Tests for HostedSiteService CRUD operations."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = HostedSiteService()
|
||||
|
||||
def test_create_site(self, db, hosting_platform, system_merchant):
|
||||
"""Test creating a hosted site with auto-created store."""
|
||||
unique = uuid.uuid4().hex[:8]
|
||||
site = self.service.create(
|
||||
db,
|
||||
{
|
||||
"business_name": f"New Business {unique}",
|
||||
"contact_email": f"test-{unique}@example.com",
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert site.id is not None
|
||||
assert site.business_name == f"New Business {unique}"
|
||||
assert site.status == HostedSiteStatus.DRAFT
|
||||
assert site.store_id is not None
|
||||
|
||||
def test_create_site_with_all_fields(self, db, hosting_platform, system_merchant):
|
||||
"""Test creating a site with all optional fields."""
|
||||
unique = uuid.uuid4().hex[:8]
|
||||
site = self.service.create(
|
||||
db,
|
||||
{
|
||||
"business_name": f"Full Business {unique}",
|
||||
"contact_name": "Jane Doe",
|
||||
"contact_email": f"jane-{unique}@example.com",
|
||||
"contact_phone": "+352 999 888",
|
||||
"internal_notes": "VIP client",
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert site.contact_name == "Jane Doe"
|
||||
assert site.contact_phone == "+352 999 888"
|
||||
assert site.internal_notes == "VIP client"
|
||||
|
||||
def test_get_by_id(self, db, hosted_site):
|
||||
"""Test getting a hosted site by ID."""
|
||||
result = self.service.get_by_id(db, hosted_site.id)
|
||||
assert result.id == hosted_site.id
|
||||
assert result.business_name == hosted_site.business_name
|
||||
|
||||
def test_get_by_id_not_found(self, db):
|
||||
"""Test getting non-existent site raises exception."""
|
||||
with pytest.raises(HostedSiteNotFoundException):
|
||||
self.service.get_by_id(db, 99999)
|
||||
|
||||
def test_get_all(self, db, hosted_site):
|
||||
"""Test listing all hosted sites."""
|
||||
sites, total = self.service.get_all(db)
|
||||
assert total >= 1
|
||||
assert len(sites) >= 1
|
||||
|
||||
def test_get_all_search(self, db, hosted_site):
|
||||
"""Test searching hosted sites by business name."""
|
||||
name_part = hosted_site.business_name[:12]
|
||||
sites, total = self.service.get_all(db, search=name_part)
|
||||
assert total >= 1
|
||||
|
||||
def test_get_all_filter_status(self, db, hosted_site):
|
||||
"""Test filtering sites by status."""
|
||||
sites, total = self.service.get_all(db, status="draft")
|
||||
assert total >= 1
|
||||
assert all(s.status == HostedSiteStatus.DRAFT for s in sites)
|
||||
|
||||
def test_get_all_pagination(self, db, hosted_site):
|
||||
"""Test pagination returns correct subset."""
|
||||
sites, total = self.service.get_all(db, page=1, per_page=1)
|
||||
assert len(sites) <= 1
|
||||
|
||||
def test_update(self, db, hosted_site):
|
||||
"""Test updating a hosted site."""
|
||||
updated = self.service.update(
|
||||
db,
|
||||
hosted_site.id,
|
||||
{"business_name": "Updated Name", "internal_notes": "Updated notes"},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert updated.business_name == "Updated Name"
|
||||
assert updated.internal_notes == "Updated notes"
|
||||
|
||||
def test_delete(self, db, hosted_site):
|
||||
"""Test deleting a hosted site."""
|
||||
site_id = hosted_site.id
|
||||
self.service.delete(db, site_id)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(HostedSiteNotFoundException):
|
||||
self.service.get_by_id(db, site_id)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestHostedSiteLifecycle:
|
||||
"""Tests for hosted site lifecycle transitions."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = HostedSiteService()
|
||||
|
||||
def test_mark_poc_ready(self, db, hosted_site):
|
||||
"""Test DRAFT → POC_READY transition."""
|
||||
site = self.service.mark_poc_ready(db, hosted_site.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.POC_READY
|
||||
|
||||
def test_send_proposal(self, db, hosted_site_poc_ready):
|
||||
"""Test POC_READY → PROPOSAL_SENT transition."""
|
||||
site = self.service.send_proposal(
|
||||
db, hosted_site_poc_ready.id, notes="Proposal notes"
|
||||
)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.PROPOSAL_SENT
|
||||
assert site.proposal_sent_at is not None
|
||||
assert site.proposal_notes == "Proposal notes"
|
||||
|
||||
def test_send_proposal_without_notes(self, db, hosted_site_poc_ready):
|
||||
"""Test sending proposal without notes."""
|
||||
site = self.service.send_proposal(db, hosted_site_poc_ready.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.PROPOSAL_SENT
|
||||
assert site.proposal_sent_at is not None
|
||||
|
||||
def test_cancel_from_draft(self, db, hosted_site):
|
||||
"""Test DRAFT → CANCELLED transition."""
|
||||
site = self.service.cancel(db, hosted_site.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.CANCELLED
|
||||
|
||||
def test_cancel_from_poc_ready(self, db, hosted_site_poc_ready):
|
||||
"""Test POC_READY → CANCELLED transition."""
|
||||
site = self.service.cancel(db, hosted_site_poc_ready.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.CANCELLED
|
||||
|
||||
def test_cancel_from_proposal_sent(self, db, hosted_site_proposal_sent):
|
||||
"""Test PROPOSAL_SENT → CANCELLED transition."""
|
||||
site = self.service.cancel(db, hosted_site_proposal_sent.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.CANCELLED
|
||||
|
||||
def test_invalid_transition_draft_to_live(self, db, hosted_site):
|
||||
"""Test that DRAFT → LIVE is not allowed."""
|
||||
with pytest.raises(InvalidStatusTransitionException):
|
||||
self.service._transition(db, hosted_site.id, HostedSiteStatus.LIVE)
|
||||
|
||||
def test_invalid_transition_draft_to_accepted(self, db, hosted_site):
|
||||
"""Test that DRAFT → ACCEPTED is not allowed."""
|
||||
with pytest.raises(InvalidStatusTransitionException):
|
||||
self.service._transition(db, hosted_site.id, HostedSiteStatus.ACCEPTED)
|
||||
|
||||
def test_invalid_transition_cancelled_to_anything(self, db, hosted_site):
|
||||
"""Test that CANCELLED state has no valid transitions."""
|
||||
self.service.cancel(db, hosted_site.id)
|
||||
db.commit()
|
||||
|
||||
with pytest.raises(InvalidStatusTransitionException):
|
||||
self.service._transition(db, hosted_site.id, HostedSiteStatus.DRAFT)
|
||||
|
||||
def test_invalid_transition_poc_ready_to_accepted(self, db, hosted_site_poc_ready):
|
||||
"""Test that POC_READY → ACCEPTED is not allowed (must go through PROPOSAL_SENT)."""
|
||||
with pytest.raises(InvalidStatusTransitionException):
|
||||
self.service._transition(
|
||||
db, hosted_site_poc_ready.id, HostedSiteStatus.ACCEPTED
|
||||
)
|
||||
|
||||
def test_suspend_from_live(self, db, hosted_site_proposal_sent):
|
||||
"""Test LIVE → SUSPENDED transition."""
|
||||
# Need to get to LIVE first: accepted → live
|
||||
site = self.service._transition(
|
||||
db, hosted_site_proposal_sent.id, HostedSiteStatus.ACCEPTED
|
||||
)
|
||||
db.flush()
|
||||
|
||||
# Manually set to LIVE (bypass go_live which needs StoreDomainService)
|
||||
site.status = HostedSiteStatus.LIVE
|
||||
db.flush()
|
||||
|
||||
site = self.service.suspend(db, site.id)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.SUSPENDED
|
||||
|
||||
def test_reactivate_from_suspended(self, db, hosted_site_proposal_sent):
|
||||
"""Test SUSPENDED → LIVE transition."""
|
||||
site = self.service._transition(
|
||||
db, hosted_site_proposal_sent.id, HostedSiteStatus.ACCEPTED
|
||||
)
|
||||
db.flush()
|
||||
|
||||
site.status = HostedSiteStatus.LIVE
|
||||
db.flush()
|
||||
|
||||
site = self.service.suspend(db, site.id)
|
||||
db.flush()
|
||||
|
||||
site = self.service._transition(db, site.id, HostedSiteStatus.LIVE)
|
||||
db.commit()
|
||||
assert site.status == HostedSiteStatus.LIVE
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestHostedSiteFromProspect:
|
||||
"""Tests for creating hosted sites from prospects."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = HostedSiteService()
|
||||
|
||||
def test_create_from_prospect(self, db, hosting_platform, system_merchant):
|
||||
"""Test creating a hosted site from a prospect."""
|
||||
from app.modules.prospecting.models import Prospect
|
||||
|
||||
prospect = Prospect(
|
||||
channel="digital",
|
||||
domain_name=f"prospect-{uuid.uuid4().hex[:8]}.lu",
|
||||
business_name="Prospect Business",
|
||||
status="active",
|
||||
has_website=True,
|
||||
)
|
||||
db.add(prospect)
|
||||
db.commit()
|
||||
db.refresh(prospect)
|
||||
|
||||
site = self.service.create_from_prospect(db, prospect.id)
|
||||
db.commit()
|
||||
|
||||
assert site.id is not None
|
||||
assert site.prospect_id == prospect.id
|
||||
assert site.business_name == "Prospect Business"
|
||||
|
||||
def test_create_from_prospect_with_contacts(
|
||||
self, db, hosting_platform, system_merchant
|
||||
):
|
||||
"""Test creating from prospect pre-fills contact info."""
|
||||
from app.modules.prospecting.models import Prospect, ProspectContact
|
||||
|
||||
prospect = Prospect(
|
||||
channel="digital",
|
||||
domain_name=f"contacts-{uuid.uuid4().hex[:8]}.lu",
|
||||
business_name="Contact Business",
|
||||
status="active",
|
||||
)
|
||||
db.add(prospect)
|
||||
db.flush()
|
||||
|
||||
email_contact = ProspectContact(
|
||||
prospect_id=prospect.id,
|
||||
contact_type="email",
|
||||
value="hello@test.lu",
|
||||
is_primary=True,
|
||||
)
|
||||
phone_contact = ProspectContact(
|
||||
prospect_id=prospect.id,
|
||||
contact_type="phone",
|
||||
value="+352 111 222",
|
||||
is_primary=True,
|
||||
)
|
||||
db.add_all([email_contact, phone_contact])
|
||||
db.commit()
|
||||
db.refresh(prospect)
|
||||
|
||||
site = self.service.create_from_prospect(db, prospect.id)
|
||||
db.commit()
|
||||
|
||||
assert site.contact_email == "hello@test.lu"
|
||||
assert site.contact_phone == "+352 111 222"
|
||||
|
||||
def test_create_from_nonexistent_prospect(
|
||||
self, db, hosting_platform, system_merchant
|
||||
):
|
||||
"""Test creating from non-existent prospect raises exception."""
|
||||
from app.modules.prospecting.exceptions import ProspectNotFoundException
|
||||
|
||||
with pytest.raises(ProspectNotFoundException):
|
||||
self.service.create_from_prospect(db, 99999)
|
||||
66
app/modules/hosting/tests/unit/test_stats_service.py
Normal file
66
app/modules/hosting/tests/unit/test_stats_service.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# app/modules/hosting/tests/unit/test_stats_service.py
|
||||
"""
|
||||
Unit tests for hosting StatsService.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.hosting.services.stats_service import StatsService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hosting
|
||||
class TestStatsService:
|
||||
"""Tests for hosting StatsService."""
|
||||
|
||||
def setup_method(self):
|
||||
self.service = StatsService()
|
||||
|
||||
def test_dashboard_stats_empty(self, db):
|
||||
"""Test dashboard stats with no data."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert stats["total_sites"] == 0
|
||||
assert stats["live_sites"] == 0
|
||||
assert stats["poc_sites"] == 0
|
||||
assert stats["active_services"] == 0
|
||||
assert stats["monthly_revenue_cents"] == 0
|
||||
assert stats["upcoming_renewals"] == 0
|
||||
|
||||
def test_dashboard_stats_with_site(self, db, hosted_site):
|
||||
"""Test dashboard stats with a draft site."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert stats["total_sites"] >= 1
|
||||
assert stats["poc_sites"] >= 1 # draft counts as POC
|
||||
assert stats["sites_by_status"].get("draft", 0) >= 1
|
||||
|
||||
def test_dashboard_stats_with_services(
|
||||
self, db, hosted_site, client_service_domain, client_service_email
|
||||
):
|
||||
"""Test dashboard stats with active services."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert stats["active_services"] >= 2
|
||||
assert stats["monthly_revenue_cents"] >= 3000 # 2500 + 500
|
||||
|
||||
def test_dashboard_stats_services_by_type(
|
||||
self, db, hosted_site, client_service_domain, client_service_email
|
||||
):
|
||||
"""Test services_by_type breakdown."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert "domain" in stats["services_by_type"]
|
||||
assert "email" in stats["services_by_type"]
|
||||
|
||||
def test_dashboard_stats_upcoming_renewals(
|
||||
self, db, hosted_site, client_service_expiring
|
||||
):
|
||||
"""Test upcoming renewals count."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert stats["upcoming_renewals"] >= 1
|
||||
|
||||
def test_dashboard_stats_sites_by_status(
|
||||
self, db, hosted_site, hosted_site_poc_ready
|
||||
):
|
||||
"""Test sites_by_status breakdown."""
|
||||
stats = self.service.get_dashboard_stats(db)
|
||||
assert isinstance(stats["sites_by_status"], dict)
|
||||
total_from_breakdown = sum(stats["sites_by_status"].values())
|
||||
assert total_from_breakdown == stats["total_sites"]
|
||||
Reference in New Issue
Block a user