import {beforeEach, describe, expect, it, vi} from 'vitest'; import {factories, getPrismaClient} from '../../../../../test/helpers'; import {DomainService} from '../../services/DomainService.js'; import * as SESService from '../../services/SESService.js'; /** * Integration tests for Domain verification and ownership checks * Tests the security feature that prevents domains from being linked to multiple projects * unless the user is a member of the project that owns the domain */ describe('Domain Verification and Ownership Tests', () => { const prisma = getPrismaClient(); // Mock SES service to avoid external AWS calls beforeEach(() => { vi.spyOn(SESService, 'verifyDomain').mockResolvedValue(['token1', 'token2', 'token3']); vi.spyOn(SESService, 'getDomainVerificationAttributes').mockResolvedValue({ status: 'Success', tokens: ['token1', 'token2', 'token3'], }); }); // ======================================== // DOMAIN OWNERSHIP CHECKS // ======================================== describe('Domain Ownership Verification', () => { it('should allow adding a domain that does not exist yet', async () => { const {project} = await factories.createUserWithProject(); const domain = 'new-domain.com'; const ownershipCheck = await DomainService.checkDomainOwnership(domain, 'any-user-id'); expect(ownershipCheck.exists).toBe(false); // Should be able to add the domain const newDomain = await DomainService.addDomain(project.id, domain); expect(newDomain.domain).toBe(domain); expect(newDomain.projectId).toBe(project.id); }); it('should detect when a domain already exists', async () => { const {user, project} = await factories.createUserWithProject(); const domain = 'existing-domain.com'; // First project adds the domain await DomainService.addDomain(project.id, domain); // Check ownership - user IS a member of the project const ownershipCheck = await DomainService.checkDomainOwnership(domain, user.id); expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.projectId).toBe(project.id); expect(ownershipCheck.projectName).toBe(project.name); expect(ownershipCheck.isMember).toBe(true); }); it('should detect when a domain exists but user is not a member', async () => { const {project: project1} = await factories.createUserWithProject(); const {user: user2} = await factories.createUserWithProject(); const domain = 'other-project-domain.com'; // Project 1 adds the domain await DomainService.addDomain(project1.id, domain); // Check ownership for User 2 (not a member of Project 1) const ownershipCheck = await DomainService.checkDomainOwnership(domain, user2.id); expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.projectId).toBe(project1.id); expect(ownershipCheck.isMember).toBe(false); }); it('should allow access when user is a member of the project that owns the domain', async () => { const {project} = await factories.createUserWithProject(); const domain = 'shared-domain.com'; // User 1 adds the domain to their project await DomainService.addDomain(project.id, domain); // Create another user and add them to the same project const user2 = await factories.createUser({email: 'user2@test.com'}); await prisma.membership.create({ data: { userId: user2.id, projectId: project.id, role: 'ADMIN', }, }); // Check ownership for User 2 (who is now a member) const ownershipCheck = await DomainService.checkDomainOwnership(domain, user2.id); expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.isMember).toBe(true); }); }); // ======================================== // PREVENTING UNAUTHORIZED DOMAIN LINKING // ======================================== describe('Preventing Unauthorized Domain Linking', () => { it('should prevent linking a domain to another project when user is not a member', async () => { const {project: project1} = await factories.createUserWithProject(); const {user: user2} = await factories.createUserWithProject(); const domain = 'protected-domain.com'; // Project 1 adds the domain await DomainService.addDomain(project1.id, domain); // User 2 tries to link the same domain to Project 2 const ownershipCheck = await DomainService.checkDomainOwnership(domain, user2.id); // The controller would use this check to deny access expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.isMember).toBe(false); // Verify the domain is still only linked to Project 1 const domains = await prisma.domain.findMany({ where: {domain}, }); expect(domains).toHaveLength(1); expect(domains[0].projectId).toBe(project1.id); }); it('should allow member to see they can access domain from the original project', async () => { const {user, project: project1} = await factories.createUserWithProject(); const domain = 'member-domain.com'; // Add domain to project 1 await DomainService.addDomain(project1.id, domain); // User creates a second project (same user, different project) const project2 = await factories.createProject(); await prisma.membership.create({ data: { userId: user.id, projectId: project2.id, role: 'OWNER', }, }); // User tries to link the domain to project 2 const ownershipCheck = await DomainService.checkDomainOwnership(domain, user.id); // Should indicate that domain exists and user IS a member expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.isMember).toBe(true); expect(ownershipCheck.projectId).toBe(project1.id); expect(ownershipCheck.projectName).toBe(project1.name); }); }); // ======================================== // DOMAIN VERIFICATION STATUS // ======================================== describe('Domain Verification Status', () => { it('should create unverified domain when first added', async () => { const {project} = await factories.createUserWithProject(); const domain = 'unverified-domain.com'; const newDomain = await DomainService.addDomain(project.id, domain); expect(newDomain.verified).toBe(false); expect(newDomain.dkimTokens).toEqual(['token1', 'token2', 'token3']); }); it('should prevent using unverified domain for sending emails', async () => { const {project} = await factories.createUserWithProject(); const domain = 'unverified-domain.com'; await DomainService.addDomain(project.id, domain); // Try to verify email domain await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not verified/i); }); it('should allow using verified domain for sending emails', async () => { const {project} = await factories.createUserWithProject(); const domain = 'verified-domain.com'; const newDomain = await DomainService.addDomain(project.id, domain); // Manually mark as verified (simulating DNS verification) await prisma.domain.update({ where: {id: newDomain.id}, data: {verified: true}, }); // Should not throw error const verifiedDomain = await DomainService.verifyEmailDomain(`sender@${domain}`, project.id); expect(verifiedDomain.verified).toBe(true); expect(verifiedDomain.domain).toBe(domain); }); it('should prevent using domain from different project', async () => { const {project: project1} = await factories.createUserWithProject(); const {project: project2} = await factories.createUserWithProject(); const domain = 'project1-domain.com'; const newDomain = await DomainService.addDomain(project1.id, domain); // Mark as verified await prisma.domain.update({ where: {id: newDomain.id}, data: {verified: true}, }); // Try to use from different project await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project2.id)).rejects.toThrow( /belongs to a different project/i, ); }); it('should prevent using unregistered domain', async () => { const {project} = await factories.createUserWithProject(); const domain = 'not-registered.com'; // Try to use domain that was never added await expect(DomainService.verifyEmailDomain(`sender@${domain}`, project.id)).rejects.toThrow(/not registered/i); }); }); // ======================================== // MULTIPLE USERS AND PROJECTS // ======================================== describe('Multiple Users and Projects Scenarios', () => { it('should handle 3 users: owner, member, and non-member', async () => { const {user: owner, project} = await factories.createUserWithProject(); const member = await factories.createUser({email: 'member@test.com'}); const nonMember = await factories.createUser({email: 'nonmember@test.com'}); const domain = 'team-domain.com'; // Owner adds domain await DomainService.addDomain(project.id, domain); // Add member to project await prisma.membership.create({ data: { userId: member.id, projectId: project.id, role: 'MEMBER', }, }); // Check ownership for each user const ownerCheck = await DomainService.checkDomainOwnership(domain, owner.id); const memberCheck = await DomainService.checkDomainOwnership(domain, member.id); const nonMemberCheck = await DomainService.checkDomainOwnership(domain, nonMember.id); expect(ownerCheck.isMember).toBe(true); expect(memberCheck.isMember).toBe(true); expect(nonMemberCheck.isMember).toBe(false); }); it('should handle user with multiple projects', async () => { const {user, project: project1} = await factories.createUserWithProject(); const project2 = await factories.createProject(); const domain = 'multi-project-domain.com'; // Add user to second project await prisma.membership.create({ data: { userId: user.id, projectId: project2.id, role: 'ADMIN', }, }); // Add domain to project 1 await DomainService.addDomain(project1.id, domain); // User is member of both projects, but domain belongs to project 1 const ownershipCheck = await DomainService.checkDomainOwnership(domain, user.id); expect(ownershipCheck.exists).toBe(true); expect(ownershipCheck.isMember).toBe(true); expect(ownershipCheck.projectId).toBe(project1.id); }); it('should handle domain being removed and re-added', async () => { const {user, project} = await factories.createUserWithProject(); const domain = 'reusable-domain.com'; // Add domain const domain1 = await DomainService.addDomain(project.id, domain); // Remove domain await DomainService.removeDomain(domain1.id); // Check ownership - should not exist const checkAfterRemoval = await DomainService.checkDomainOwnership(domain, user.id); expect(checkAfterRemoval.exists).toBe(false); // Re-add domain const domain2 = await DomainService.addDomain(project.id, domain); expect(domain2.domain).toBe(domain); expect(domain2.projectId).toBe(project.id); }); }); // ======================================== // ROLE-BASED RESTRICTIONS // ======================================== describe('Role-Based Domain Access', () => { it('should verify that only ADMIN and OWNER can add domains', async () => { const {project} = await factories.createUserWithProject(); const regularMember = await factories.createUser({email: 'member@test.com'}); await prisma.membership.create({ data: { userId: regularMember.id, projectId: project.id, role: 'MEMBER', }, }); // The controller checks for role: { in: ['ADMIN', 'OWNER'] } // This test verifies the database structure supports this check const membership = await prisma.membership.findFirst({ where: { userId: regularMember.id, projectId: project.id, role: { in: ['ADMIN', 'OWNER'], }, }, }); expect(membership).toBeNull(); }); it('should verify that ADMIN and OWNER roles can add domains', async () => { const {user: owner, project} = await factories.createUserWithProject(); const admin = await factories.createUser({email: 'admin@test.com'}); await prisma.membership.create({ data: { userId: admin.id, projectId: project.id, role: 'ADMIN', }, }); // Verify both owner and admin have required roles const ownerMembership = await prisma.membership.findFirst({ where: { userId: owner.id, projectId: project.id, role: { in: ['ADMIN', 'OWNER'], }, }, }); const adminMembership = await prisma.membership.findFirst({ where: { userId: admin.id, projectId: project.id, role: { in: ['ADMIN', 'OWNER'], }, }, }); expect(ownerMembership).not.toBeNull(); expect(adminMembership).not.toBeNull(); }); }); // ======================================== // EDGE CASES // ======================================== describe('Edge Cases', () => { it('should handle case-sensitive domain names', async () => { const {project} = await factories.createUserWithProject(); // Domains are typically case-insensitive in DNS, but stored as-is in DB const domain1 = await DomainService.addDomain(project.id, 'Example.com'); expect(domain1.domain).toBe('Example.com'); }); it('should handle subdomain vs root domain', async () => { const {project} = await factories.createUserWithProject(); const rootDomain = await DomainService.addDomain(project.id, 'example.com'); const subDomain = await DomainService.addDomain(project.id, 'mail.example.com'); expect(rootDomain.domain).toBe('example.com'); expect(subDomain.domain).toBe('mail.example.com'); // Both should be separate entries const domains = await prisma.domain.findMany({ where: {projectId: project.id}, }); expect(domains).toHaveLength(2); }); it('should handle invalid email format in verifyEmailDomain', async () => { const {project} = await factories.createUserWithProject(); await expect(DomainService.verifyEmailDomain('not-an-email', project.id)).rejects.toThrow( /invalid email format/i, ); await expect(DomainService.verifyEmailDomain('multiple@at@signs.com', project.id)).rejects.toThrow( /invalid email format/i, ); }); }); // ======================================== // GET PROJECT DOMAINS // ======================================== describe('Get Project Domains', () => { it('should return all domains for a project', async () => { const {project} = await factories.createUserWithProject(); await DomainService.addDomain(project.id, 'domain1.com'); await DomainService.addDomain(project.id, 'domain2.com'); await DomainService.addDomain(project.id, 'domain3.com'); const domains = await DomainService.getProjectDomains(project.id); expect(domains).toHaveLength(3); expect(domains.map(d => d.domain).sort()).toEqual(['domain1.com', 'domain2.com', 'domain3.com']); }); it('should return empty array for project with no domains', async () => { const {project} = await factories.createUserWithProject(); const domains = await DomainService.getProjectDomains(project.id); expect(domains).toHaveLength(0); }); it('should not return domains from other projects', async () => { const {project: project1} = await factories.createUserWithProject(); const {project: project2} = await factories.createUserWithProject(); await DomainService.addDomain(project1.id, 'project1-domain.com'); await DomainService.addDomain(project2.id, 'project2-domain.com'); const project1Domains = await DomainService.getProjectDomains(project1.id); const project2Domains = await DomainService.getProjectDomains(project2.id); expect(project1Domains).toHaveLength(1); expect(project1Domains[0].domain).toBe('project1-domain.com'); expect(project2Domains).toHaveLength(1); expect(project2Domains[0].domain).toBe('project2-domain.com'); }); }); });