# tests/unit/services/test_merchant_domain_service.py """Unit tests for MerchantDomainService.""" import uuid from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest from pydantic import ValidationError from app.modules.tenancy.exceptions import ( DNSVerificationException, DomainAlreadyVerifiedException, DomainNotVerifiedException, DomainVerificationFailedException, MaxDomainsReachedException, MerchantDomainAlreadyExistsException, MerchantDomainNotFoundException, MerchantNotFoundException, ) from app.modules.tenancy.models.merchant_domain import MerchantDomain from app.modules.tenancy.models.store_domain import StoreDomain from app.modules.tenancy.schemas.merchant_domain import ( MerchantDomainCreate, MerchantDomainUpdate, ) from app.modules.tenancy.services.merchant_domain_service import ( merchant_domain_service, ) # ============================================================================= # ADD DOMAIN TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceAdd: """Test suite for adding merchant domains.""" def test_add_domain_success(self, db, test_merchant): """Test successfully adding a domain to a merchant.""" unique_id = str(uuid.uuid4())[:8] domain_data = MerchantDomainCreate( domain=f"newmerchant{unique_id}.example.com", is_primary=True, ) result = merchant_domain_service.add_domain( db, test_merchant.id, domain_data ) db.commit() assert result is not None assert result.merchant_id == test_merchant.id assert result.domain == f"newmerchant{unique_id}.example.com" assert result.is_primary is True assert result.is_verified is False assert result.is_active is False assert result.verification_token is not None def test_add_domain_merchant_not_found(self, db): """Test adding domain to non-existent merchant raises exception.""" domain_data = MerchantDomainCreate( domain="test.example.com", is_primary=True, ) with pytest.raises(MerchantNotFoundException): merchant_domain_service.add_domain(db, 99999, domain_data) def test_add_domain_already_exists_as_merchant_domain( self, db, test_merchant ): """Test adding a domain that already exists as MerchantDomain raises exception.""" unique_id = str(uuid.uuid4())[:8] existing = MerchantDomain( merchant_id=test_merchant.id, domain=f"existing{unique_id}.example.com", verification_token=f"ext_{unique_id}", ) db.add(existing) db.commit() domain_data = MerchantDomainCreate( domain=f"existing{unique_id}.example.com", is_primary=True, ) with pytest.raises(MerchantDomainAlreadyExistsException): merchant_domain_service.add_domain( db, test_merchant.id, domain_data ) def test_add_domain_already_exists_as_store_domain( self, db, test_merchant, test_store ): """Test adding a domain that already exists as StoreDomain raises exception.""" unique_id = str(uuid.uuid4())[:8] sd = StoreDomain( store_id=test_store.id, domain=f"storeexist{unique_id}.example.com", verification_token=f"se_{unique_id}", ) db.add(sd) db.commit() domain_data = MerchantDomainCreate( domain=f"storeexist{unique_id}.example.com", is_primary=True, ) with pytest.raises(MerchantDomainAlreadyExistsException): merchant_domain_service.add_domain( db, test_merchant.id, domain_data ) def test_add_domain_max_limit_reached(self, db, test_merchant): """Test adding domain when max limit reached raises exception.""" for i in range(merchant_domain_service.max_domains_per_merchant): domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"limit{i}_{uuid.uuid4().hex[:6]}.example.com", verification_token=f"lim_{i}_{uuid.uuid4().hex[:6]}", ) db.add(domain) db.commit() domain_data = MerchantDomainCreate( domain="onemore.example.com", is_primary=True, ) with pytest.raises(MaxDomainsReachedException): merchant_domain_service.add_domain( db, test_merchant.id, domain_data ) def test_add_domain_reserved_subdomain(self): """Test adding a domain with reserved subdomain is rejected by schema.""" with pytest.raises(ValidationError) as exc_info: MerchantDomainCreate( domain="admin.example.com", is_primary=True, ) assert "reserved subdomain" in str(exc_info.value).lower() def test_add_domain_sets_primary_unsets_others(self, db, test_merchant): """Test adding a primary domain unsets other primary domains.""" unique_id = str(uuid.uuid4())[:8] first = MerchantDomain( merchant_id=test_merchant.id, domain=f"first{unique_id}.example.com", is_primary=True, verification_token=f"f_{unique_id}", ) db.add(first) db.commit() domain_data = MerchantDomainCreate( domain=f"second{unique_id}.example.com", is_primary=True, ) result = merchant_domain_service.add_domain( db, test_merchant.id, domain_data ) db.commit() db.refresh(first) assert result.is_primary is True assert first.is_primary is False # ============================================================================= # GET DOMAINS TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceGet: """Test suite for getting merchant domains.""" def test_get_merchant_domains_success(self, db, test_merchant): """Test getting all domains for a merchant.""" unique_id = str(uuid.uuid4())[:8] d1 = MerchantDomain( merchant_id=test_merchant.id, domain=f"get1{unique_id}.example.com", verification_token=f"g1_{unique_id}", ) d2 = MerchantDomain( merchant_id=test_merchant.id, domain=f"get2{unique_id}.example.com", verification_token=f"g2_{unique_id}", is_primary=False, ) db.add_all([d1, d2]) db.commit() domains = merchant_domain_service.get_merchant_domains( db, test_merchant.id ) domain_names = [d.domain for d in domains] assert f"get1{unique_id}.example.com" in domain_names assert f"get2{unique_id}.example.com" in domain_names def test_get_merchant_domains_merchant_not_found(self, db): """Test getting domains for non-existent merchant raises exception.""" with pytest.raises(MerchantNotFoundException): merchant_domain_service.get_merchant_domains(db, 99999) def test_get_domain_by_id_success(self, db, test_merchant): """Test getting a domain by ID.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"byid{unique_id}.example.com", verification_token=f"bi_{unique_id}", ) db.add(domain) db.commit() db.refresh(domain) result = merchant_domain_service.get_domain_by_id(db, domain.id) assert result.id == domain.id def test_get_domain_by_id_not_found(self, db): """Test getting non-existent domain raises exception.""" with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.get_domain_by_id(db, 99999) # ============================================================================= # UPDATE DOMAIN TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceUpdate: """Test suite for updating merchant domains.""" def test_update_domain_set_primary(self, db, test_merchant): """Test setting a domain as primary.""" unique_id = str(uuid.uuid4())[:8] d1 = MerchantDomain( merchant_id=test_merchant.id, domain=f"upd1{unique_id}.example.com", is_primary=True, is_verified=True, verified_at=datetime.now(UTC), verification_token=f"u1_{unique_id}", ) d2 = MerchantDomain( merchant_id=test_merchant.id, domain=f"upd2{unique_id}.example.com", is_primary=False, is_verified=True, verified_at=datetime.now(UTC), verification_token=f"u2_{unique_id}", ) db.add_all([d1, d2]) db.commit() update_data = MerchantDomainUpdate(is_primary=True) result = merchant_domain_service.update_domain( db, d2.id, update_data ) db.commit() db.refresh(d1) assert result.is_primary is True assert d1.is_primary is False def test_update_domain_activate_unverified_fails(self, db, test_merchant): """Test activating an unverified domain raises exception.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"unvact{unique_id}.example.com", is_verified=False, is_active=False, verification_token=f"ua_{unique_id}", ) db.add(domain) db.commit() update_data = MerchantDomainUpdate(is_active=True) with pytest.raises(DomainNotVerifiedException): merchant_domain_service.update_domain(db, domain.id, update_data) def test_update_domain_activate_verified(self, db, test_merchant): """Test activating a verified domain succeeds.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"veract{unique_id}.example.com", is_verified=True, verified_at=datetime.now(UTC), is_active=False, verification_token=f"va_{unique_id}", ) db.add(domain) db.commit() update_data = MerchantDomainUpdate(is_active=True) result = merchant_domain_service.update_domain( db, domain.id, update_data ) db.commit() assert result.is_active is True def test_update_domain_deactivate(self, db, test_merchant): """Test deactivating a domain.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"deact{unique_id}.example.com", is_verified=True, verified_at=datetime.now(UTC), is_active=True, verification_token=f"da_{unique_id}", ) db.add(domain) db.commit() update_data = MerchantDomainUpdate(is_active=False) result = merchant_domain_service.update_domain( db, domain.id, update_data ) db.commit() assert result.is_active is False def test_update_domain_not_found(self, db): """Test updating non-existent domain raises exception.""" update_data = MerchantDomainUpdate(is_primary=True) with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.update_domain(db, 99999, update_data) # ============================================================================= # DELETE DOMAIN TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceDelete: """Test suite for deleting merchant domains.""" def test_delete_domain_success(self, db, test_merchant): """Test successfully deleting a domain.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"todel{unique_id}.example.com", verification_token=f"td_{unique_id}", ) db.add(domain) db.commit() domain_id = domain.id result = merchant_domain_service.delete_domain(db, domain_id) db.commit() assert "deleted successfully" in result with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.get_domain_by_id(db, domain_id) def test_delete_domain_not_found(self, db): """Test deleting non-existent domain raises exception.""" with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.delete_domain(db, 99999) # ============================================================================= # VERIFY DOMAIN TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceVerify: """Test suite for merchant domain verification.""" @patch("dns.resolver.resolve") def test_verify_domain_success(self, mock_resolve, db, test_merchant): """Test successful domain verification.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"verify{unique_id}.example.com", is_verified=False, verification_token=f"vt_{unique_id}", ) db.add(domain) db.commit() mock_txt = MagicMock() mock_txt.to_text.return_value = f'"vt_{unique_id}"' mock_resolve.return_value = [mock_txt] result_domain, message = merchant_domain_service.verify_domain( db, domain.id ) db.commit() assert result_domain.is_verified is True assert result_domain.verified_at is not None assert "verified successfully" in message.lower() def test_verify_domain_already_verified(self, db, test_merchant): """Test verifying already verified domain raises exception.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"alrver{unique_id}.example.com", is_verified=True, verified_at=datetime.now(UTC), verification_token=f"av_{unique_id}", ) db.add(domain) db.commit() with pytest.raises(DomainAlreadyVerifiedException): merchant_domain_service.verify_domain(db, domain.id) def test_verify_domain_not_found(self, db): """Test verifying non-existent domain raises exception.""" with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.verify_domain(db, 99999) @patch("dns.resolver.resolve") def test_verify_domain_token_not_found( self, mock_resolve, db, test_merchant ): """Test verification fails when token not found in DNS.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"tnf{unique_id}.example.com", is_verified=False, verification_token=f"tnf_{unique_id}", ) db.add(domain) db.commit() mock_txt = MagicMock() mock_txt.to_text.return_value = '"wrong_token"' mock_resolve.return_value = [mock_txt] with pytest.raises(DomainVerificationFailedException) as exc_info: merchant_domain_service.verify_domain(db, domain.id) assert "token not found" in str(exc_info.value).lower() @patch("dns.resolver.resolve") def test_verify_domain_dns_nxdomain( self, mock_resolve, db, test_merchant ): """Test verification fails when DNS record doesn't exist.""" import dns.resolver unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"nxdom{unique_id}.example.com", is_verified=False, verification_token=f"nx_{unique_id}", ) db.add(domain) db.commit() mock_resolve.side_effect = dns.resolver.NXDOMAIN() with pytest.raises(DomainVerificationFailedException): merchant_domain_service.verify_domain(db, domain.id) # ============================================================================= # VERIFICATION INSTRUCTIONS TESTS # ============================================================================= @pytest.mark.unit @pytest.mark.tenancy class TestMerchantDomainServiceInstructions: """Test suite for verification instructions.""" def test_get_verification_instructions(self, db, test_merchant): """Test getting verification instructions.""" unique_id = str(uuid.uuid4())[:8] domain = MerchantDomain( merchant_id=test_merchant.id, domain=f"instr{unique_id}.example.com", verification_token=f"inst_{unique_id}", ) db.add(domain) db.commit() instructions = merchant_domain_service.get_verification_instructions( db, domain.id ) assert instructions["domain"] == f"instr{unique_id}.example.com" assert instructions["verification_token"] == f"inst_{unique_id}" assert "instructions" in instructions assert "txt_record" in instructions assert instructions["txt_record"]["type"] == "TXT" assert instructions["txt_record"]["name"] == "_wizamart-verify" assert "common_registrars" in instructions def test_get_verification_instructions_not_found(self, db): """Test getting instructions for non-existent domain raises exception.""" with pytest.raises(MerchantDomainNotFoundException): merchant_domain_service.get_verification_instructions(db, 99999)