367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
/**
|
|
* 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: '<p>Thanks for signing up</p>',
|
|
from: '[email protected]',
|
|
};
|
|
|
|
// 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: '[email protected]',
|
|
});
|
|
|
|
// Try to create another contact with the same email (violates unique constraint)
|
|
await expect(
|
|
prisma.contact.create({
|
|
data: {
|
|
projectId,
|
|
email: '[email protected]', // 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');
|
|
});
|
|
});
|
|
});
|