feat(tenancy): add merchant-level domain with store override
Merchants can now register domains (e.g., myloyaltyprogram.lu) that all their stores inherit. Individual stores can override with their own custom domain. Resolution priority: StoreDomain > MerchantDomain > subdomain. - Add MerchantDomain model, schema, service, and admin API endpoints - Add merchant domain fallback in platform and store context middleware - Add Merchant.primary_domain and Store.effective_domain properties - Add Alembic migration for merchant_domains table - Update loyalty user journey docs with subscription & domain setup flow - Add unit tests (50 passing) and integration tests (15 passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
219
tests/integration/api/v1/admin/test_merchant_domains.py
Normal file
219
tests/integration/api/v1/admin/test_merchant_domains.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# tests/integration/api/v1/admin/test_merchant_domains.py
|
||||
"""Integration tests for admin merchant domain management endpoints.
|
||||
|
||||
Tests the /api/v1/admin/merchants/{id}/domains/* and
|
||||
/api/v1/admin/merchants/domains/merchant/* endpoints.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.tenancy.models.merchant_domain import MerchantDomain
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.admin
|
||||
class TestAdminMerchantDomainsAPI:
|
||||
"""Test admin merchant domain management endpoints."""
|
||||
|
||||
def test_create_merchant_domain(self, client, admin_headers, test_merchant):
|
||||
"""Test POST create merchant domain returns 201-equivalent (200 with data)."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
response = client.post(
|
||||
f"/api/v1/admin/merchants/{test_merchant.id}/domains",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"domain": f"newmerch{unique_id}.example.com",
|
||||
"is_primary": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["domain"] == f"newmerch{unique_id}.example.com"
|
||||
assert data["merchant_id"] == test_merchant.id
|
||||
assert data["is_primary"] is True
|
||||
assert data["is_verified"] is False
|
||||
assert data["is_active"] is False
|
||||
assert data["verification_token"] is not None
|
||||
|
||||
def test_create_duplicate_domain(
|
||||
self, client, admin_headers, test_merchant, db
|
||||
):
|
||||
"""Test POST create duplicate domain returns 409 conflict."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain_name = f"dup{unique_id}.example.com"
|
||||
|
||||
# Create existing domain
|
||||
existing = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=domain_name,
|
||||
verification_token=f"dup_{unique_id}",
|
||||
)
|
||||
db.add(existing)
|
||||
db.commit()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/admin/merchants/{test_merchant.id}/domains",
|
||||
headers=admin_headers,
|
||||
json={"domain": domain_name, "is_primary": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
data = response.json()
|
||||
assert data["error_code"] == "MERCHANT_DOMAIN_ALREADY_EXISTS"
|
||||
|
||||
def test_create_domain_invalid_format(
|
||||
self, client, admin_headers, test_merchant
|
||||
):
|
||||
"""Test POST create domain with invalid format returns 422."""
|
||||
response = client.post(
|
||||
f"/api/v1/admin/merchants/{test_merchant.id}/domains",
|
||||
headers=admin_headers,
|
||||
json={"domain": "notadomain", "is_primary": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_create_domain_merchant_not_found(self, client, admin_headers):
|
||||
"""Test POST create domain for non-existent merchant returns 404."""
|
||||
response = client.post(
|
||||
"/api/v1/admin/merchants/99999/domains",
|
||||
headers=admin_headers,
|
||||
json={"domain": "test.example.com", "is_primary": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_list_merchant_domains(
|
||||
self, client, admin_headers, test_merchant, db
|
||||
):
|
||||
"""Test GET list merchant domains returns 200."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"list{unique_id}.example.com",
|
||||
verification_token=f"list_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1/admin/merchants/{test_merchant.id}/domains",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] >= 1
|
||||
domain_names = [d["domain"] for d in data["domains"]]
|
||||
assert f"list{unique_id}.example.com" in domain_names
|
||||
|
||||
def test_get_domain_detail(self, client, admin_headers, test_merchant, db):
|
||||
"""Test GET domain detail returns 200."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"detail{unique_id}.example.com",
|
||||
verification_token=f"det_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1/admin/merchants/domains/merchant/{domain.id}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == domain.id
|
||||
assert data["domain"] == f"detail{unique_id}.example.com"
|
||||
|
||||
def test_update_domain(self, client, admin_headers, test_merchant, db):
|
||||
"""Test PUT update domain returns 200."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"update{unique_id}.example.com",
|
||||
is_verified=True,
|
||||
verified_at=datetime.now(UTC),
|
||||
is_active=False,
|
||||
is_primary=False,
|
||||
verification_token=f"upd_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1/admin/merchants/domains/merchant/{domain.id}",
|
||||
headers=admin_headers,
|
||||
json={"is_active": True, "is_primary": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_active"] is True
|
||||
assert data["is_primary"] is True
|
||||
|
||||
def test_update_activate_unverified_domain(
|
||||
self, client, admin_headers, test_merchant, db
|
||||
):
|
||||
"""Test PUT activate unverified domain returns 400."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"unver{unique_id}.example.com",
|
||||
is_verified=False,
|
||||
is_active=False,
|
||||
verification_token=f"unv_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1/admin/merchants/domains/merchant/{domain.id}",
|
||||
headers=admin_headers,
|
||||
json={"is_active": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["error_code"] == "DOMAIN_NOT_VERIFIED"
|
||||
|
||||
def test_delete_domain(self, client, admin_headers, test_merchant, db):
|
||||
"""Test DELETE domain returns 200."""
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
domain = MerchantDomain(
|
||||
merchant_id=test_merchant.id,
|
||||
domain=f"del{unique_id}.example.com",
|
||||
verification_token=f"del_{unique_id}",
|
||||
)
|
||||
db.add(domain)
|
||||
db.commit()
|
||||
db.refresh(domain)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1/admin/merchants/domains/merchant/{domain.id}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "deleted" in data["message"].lower()
|
||||
assert data["merchant_id"] == test_merchant.id
|
||||
|
||||
def test_non_admin_access(self, client, auth_headers, test_merchant):
|
||||
"""Test non-admin access returns 403."""
|
||||
response = client.get(
|
||||
f"/api/v1/admin/merchants/{test_merchant.id}/domains",
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
Reference in New Issue
Block a user