/** * Example Test File * * This file demonstrates common testing patterns used in the Plunk V2 codebase. * Use this as a reference when writing new tests. */ import {describe, it, expect, beforeEach, afterEach} from 'vitest'; import {factories, getPrismaClient, createTimeControl, createMockQueues, createServiceMocks} from '../helpers'; import {CampaignStatus, WorkflowStepType, EmailStatus, StepExecutionStatus} from '@plunk/db'; describe('Example Tests - Common Patterns', () => { let projectId: string; const prisma = getPrismaClient(); const timeControl = createTimeControl(); beforeEach(async () => { // Create fresh test data before each test const {project} = await factories.createUserWithProject(); projectId = project.id; }); afterEach(() => { // Restore real timers after each test timeControl.restore(); }); describe('Pattern 1: Basic CRUD Testing', () => { it('should create and retrieve an entity', async () => { // Arrange: Set up test data const campaignData = { name: 'Welcome Campaign', subject: 'Welcome to Plunk!', body: '

Thanks for signing up

', from: 'hello@example.com', }; // Act: Create the campaign const campaign = await factories.createCampaign({ projectId, ...campaignData, }); // Assert: Verify it was created correctly expect(campaign.id).toBeDefined(); expect(campaign.name).toBe('Welcome Campaign'); expect(campaign.status).toBe(CampaignStatus.DRAFT); // Act: Retrieve it const retrieved = await prisma.campaign.findUnique({ where: {id: campaign.id}, }); // Assert: Verify retrieval expect(retrieved).not.toBeNull(); expect(retrieved?.name).toBe('Welcome Campaign'); }); it('should update an entity', async () => { const campaign = await factories.createCampaign({ projectId, name: 'Original Name', }); // Update const updated = await prisma.campaign.update({ where: {id: campaign.id}, data: {name: 'Updated Name'}, }); expect(updated.name).toBe('Updated Name'); }); it('should delete an entity', async () => { const campaign = await factories.createCampaign({projectId}); // Delete await prisma.campaign.delete({ where: {id: campaign.id}, }); // Verify deletion const deleted = await prisma.campaign.findUnique({ where: {id: campaign.id}, }); expect(deleted).toBeNull(); }); }); describe('Pattern 2: Testing Relationships', () => { it('should create entities with relationships', async () => { // Create related entities const segment = await factories.createSegment(projectId, { name: 'Premium Users', }); const campaign = await factories.createCampaign({ projectId, segmentId: segment.id, }); // Verify relationship const campaignWithSegment = await prisma.campaign.findUnique({ where: {id: campaign.id}, include: {segment: true}, }); expect(campaignWithSegment?.segment).toBeDefined(); expect(campaignWithSegment?.segment?.name).toBe('Premium Users'); }); it('should handle one-to-many relationships', async () => { const contact = await factories.createContact({projectId}); // Create multiple emails for one contact const email1 = await factories.createEmail(projectId, contact.id); const email2 = await factories.createEmail(projectId, contact.id); const email3 = await factories.createEmail(projectId, contact.id); // Verify all emails are associated with the contact const contactWithEmails = await prisma.contact.findUnique({ where: {id: contact.id}, include: {emails: true}, }); expect(contactWithEmails?.emails).toHaveLength(3); }); }); describe('Pattern 3: Testing with Time Control', () => { it('should schedule something for the future', async () => { // Freeze time at a known point const now = timeControl.freeze(new Date('2025-01-20T10:00:00Z')); // Schedule for 2 hours from now const scheduledTime = timeControl.helpers.relative(2, 'hour'); const campaign = await factories.createScheduledCampaign(projectId, scheduledTime, {name: 'Future Campaign'}); // Verify it's scheduled for the future expect(campaign.scheduledFor).toEqual(scheduledTime); expect(campaign.status).toBe(CampaignStatus.SCHEDULED); // Advance time by 1 hour (not yet time to run) timeControl.helpers.advanceHours(1); expect(timeControl.now()).toEqual(new Date('2025-01-20T11:00:00Z')); // Advance to scheduled time timeControl.advanceTo(scheduledTime); expect(timeControl.now()).toEqual(scheduledTime); }); it('should handle timeout scenarios', async () => { timeControl.freeze(new Date('2025-01-20T10:00:00Z')); const template = await factories.createTemplate({projectId}); const {workflow, steps} = await factories.createWorkflowWithSteps(projectId, [ {type: WorkflowStepType.WAIT_FOR_EVENT, timeout: 3600}, // 1 hour timeout ]); const contact = await factories.createContact({projectId}); const execution = await factories.createWorkflowExecution(workflow.id, contact.id); const stepExecution = await prisma.workflowStepExecution.create({ data: { executionId: execution.id, stepId: steps[0].id, status: StepExecutionStatus.WAITING, startedAt: new Date(), }, }); // Advance past timeout timeControl.helpers.advanceHours(1); // Verify we can detect timeout const elapsed = timeControl.now().getTime() - (stepExecution.startedAt?.getTime() || 0); const hasTimedOut = elapsed >= 3600000; // 1 hour in milliseconds expect(hasTimedOut).toBe(true); }); }); describe('Pattern 4: Testing Collections and Pagination', () => { it('should handle pagination correctly', async () => { // Create 25 campaigns const campaigns = []; for (let i = 0; i < 25; i++) { campaigns.push( await factories.createCampaign({ projectId, name: `Campaign ${i}`, }), ); } // Test first page const page1 = await prisma.campaign.findMany({ where: {projectId}, orderBy: {createdAt: 'desc'}, take: 10, skip: 0, }); expect(page1).toHaveLength(10); // Test second page const page2 = await prisma.campaign.findMany({ where: {projectId}, orderBy: {createdAt: 'desc'}, take: 10, skip: 10, }); expect(page2).toHaveLength(10); // Verify no overlap const page1Ids = page1.map(c => c.id); const page2Ids = page2.map(c => c.id); const overlap = page1Ids.filter(id => page2Ids.includes(id)); expect(overlap).toHaveLength(0); }); it('should filter collections', async () => { // Create campaigns with different statuses await factories.createCampaign({projectId, status: CampaignStatus.DRAFT}); await factories.createCampaign({projectId, status: CampaignStatus.DRAFT}); await factories.createCampaign({projectId, status: CampaignStatus.SENT}); await factories.createCampaign({projectId, status: CampaignStatus.SENT}); await factories.createCampaign({projectId, status: CampaignStatus.SCHEDULED}); // Filter by status const drafts = await prisma.campaign.findMany({ where: {projectId, status: CampaignStatus.DRAFT}, }); const sent = await prisma.campaign.findMany({ where: {projectId, status: CampaignStatus.SENT}, }); expect(drafts).toHaveLength(2); expect(sent).toHaveLength(2); }); }); describe('Pattern 5: Testing State Transitions', () => { it('should track status changes', async () => { const contact = await factories.createContact({projectId}); const email = await factories.createEmail(projectId, contact.id, { status: EmailStatus.PENDING, }); // Verify initial state expect(email.status).toBe(EmailStatus.PENDING); // Transition to SENDING const sending = await prisma.email.update({ where: {id: email.id}, data: {status: EmailStatus.SENDING}, }); expect(sending.status).toBe(EmailStatus.SENDING); // Transition to SENT const sent = await prisma.email.update({ where: {id: email.id}, data: { status: EmailStatus.SENT, sentAt: new Date(), }, }); expect(sent.status).toBe(EmailStatus.SENT); expect(sent.sentAt).toBeDefined(); }); }); describe('Pattern 6: Testing Bulk Operations', () => { it('should handle bulk creates efficiently', async () => { // Create many contacts at once const contacts = await factories.createContacts(projectId, 100); expect(contacts).toHaveLength(100); // Verify they were all created const count = await prisma.contact.count({ where: {projectId}, }); expect(count).toBe(100); }); it('should handle bulk updates', async () => { const contacts = await factories.createContacts(projectId, 50); // Bulk update all contacts await prisma.contact.updateMany({ where: {projectId}, data: {subscribed: false}, }); // Verify all were updated const unsubscribed = await prisma.contact.findMany({ where: {projectId, subscribed: false}, }); expect(unsubscribed).toHaveLength(50); }); }); describe('Pattern 7: Testing Error Cases', () => { it('should handle not found errors', async () => { const result = await prisma.campaign.findUnique({ where: {id: 'non-existent-id'}, }); expect(result).toBeNull(); }); it('should handle validation errors', async () => { // Create a contact first await factories.createContact({ projectId, email: 'test@example.com', }); // Try to create another contact with the same email (violates unique constraint) await expect( prisma.contact.create({ data: { projectId, email: 'test@example.com', // Duplicate email - violates unique constraint on [projectId, email] }, }), ).rejects.toThrow(); }); }); describe('Pattern 8: Testing Complex Workflows', () => { it('should execute a multi-step workflow', async () => { const template = await factories.createTemplate({projectId}); // Create workflow with multiple steps const {workflow, steps} = await factories.createWorkflowWithSteps(projectId, [ {type: WorkflowStepType.SEND_EMAIL, templateId: template.id}, {type: WorkflowStepType.DELAY, delay: 3600}, // 1 hour delay {type: WorkflowStepType.SEND_EMAIL, templateId: template.id}, ]); expect(workflow).toBeDefined(); expect(steps).toHaveLength(3); // Verify step order expect(steps[0].type).toBe(WorkflowStepType.SEND_EMAIL); expect(steps[1].type).toBe(WorkflowStepType.DELAY); expect(steps[2].type).toBe(WorkflowStepType.SEND_EMAIL); // Verify delay configuration in the config JSON field const delayConfig = steps[1].config as {amount: number; unit: string}; expect(delayConfig.amount).toBe(3600); expect(delayConfig.unit).toBe('hours'); }); }); });