Initial push of Plunk Next

This commit is contained in:
Dries Augustyns
2025-12-01 09:56:56 +01:00
parent 07cea20262
commit ff1876d580
566 changed files with 89036 additions and 28423 deletions
@@ -0,0 +1,462 @@
import {describe, it, expect, beforeEach, 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 {user, 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: '[email protected]'});
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, project: project2} = 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: '[email protected]'});
const nonMember = await factories.createUser({email: '[email protected]'});
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: '[email protected]'});
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: '[email protected]'});
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@[email protected]', 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');
});
});
});