Skip to content

Integration Testing Guidelines

Overview

This document outlines integration testing practices for our SaaS CRM application, covering API testing, database integration, end-to-end workflows, and third-party service integration.

Integration Testing Scope

What to Test

  • API endpoint integration
  • Database transactions
  • Service layer interactions
  • Authentication flows
  • Third-party API integrations
  • Message queue processing
  • Cache layer functionality
  • File upload/download
  • Email sending
  • Cross-service communication

API Integration Testing

REST API Testing Setup

// tests/integration/api/setup.ts
import { beforeAll, afterAll, beforeEach } from 'vitest'
import { setupServer } from 'msw/node'
import { db } from './db'

export const server = setupServer()

beforeAll(() => {
  server.listen({ onUnhandledRequest: 'error' })
  db.migrate.latest()
})

beforeEach(() => {
  server.resetHandlers()
  db.seed.run()
})

afterAll(() => {
  server.close()
  db.destroy()
})

API Endpoint Testing

// tests/integration/api/customers.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createApp } from '~/server'
import request from 'supertest'
import { generateToken } from '~/utils/auth'

describe('Customer API Integration', () => {
  let app: any
  let authToken: string

  beforeEach(async () => {
    app = await createApp()
    authToken = generateToken({ id: 1, role: 'admin' })
  })

  describe('GET /api/customers', () => {
    it('should return paginated customers', async () => {
      const response = await request(app)
        .get('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .query({ page: 1, limit: 10 })
        .expect(200)

      expect(response.body).toHaveProperty('data')
      expect(response.body.data).toBeInstanceOf(Array)
      expect(response.body.data.length).toBeLessThanOrEqual(10)
      expect(response.body).toHaveProperty('meta')
      expect(response.body.meta).toHaveProperty('total')
      expect(response.body.meta).toHaveProperty('current_page')
    })

    it('should filter customers by status', async () => {
      const response = await request(app)
        .get('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .query({ status: 'active' })
        .expect(200)

      response.body.data.forEach((customer: any) => {
        expect(customer.status).toBe('active')
      })
    })

    it('should return 401 without authentication', async () => {
      await request(app)
        .get('/api/customers')
        .expect(401)
    })
  })

  describe('POST /api/customers', () => {
    it('should create a new customer', async () => {
      const newCustomer = {
        name: 'Integration Test Company',
        email: 'integration@test.com',
        phone: '555-0100',
        address: {
          street: '123 Test St',
          city: 'Test City',
          state: 'TS',
          zip: '12345'
        }
      }

      const response = await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send(newCustomer)
        .expect(201)

      expect(response.body.data).toHaveProperty('id')
      expect(response.body.data.name).toBe(newCustomer.name)
      expect(response.body.data.email).toBe(newCustomer.email)

      // Verify in database
      const dbCustomer = await db('customers')
        .where('id', response.body.data.id)
        .first()

      expect(dbCustomer).toBeTruthy()
      expect(dbCustomer.name).toBe(newCustomer.name)
    })

    it('should validate required fields', async () => {
      const invalidCustomer = {
        name: 'Test Company'
        // Missing required email
      }

      const response = await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send(invalidCustomer)
        .expect(422)

      expect(response.body.error).toHaveProperty('details')
      expect(response.body.error.details).toHaveProperty('email')
    })

    it('should prevent duplicate emails', async () => {
      const customer = {
        name: 'Test Company',
        email: 'duplicate@test.com'
      }

      // Create first customer
      await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send(customer)
        .expect(201)

      // Attempt to create duplicate
      const response = await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send(customer)
        .expect(422)

      expect(response.body.error.message).toContain('already exists')
    })
  })

  describe('PUT /api/customers/:id', () => {
    it('should update customer data', async () => {
      // Create customer first
      const createResponse = await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'Original Name',
          email: 'original@test.com'
        })
        .expect(201)

      const customerId = createResponse.body.data.id

      // Update customer
      const updateResponse = await request(app)
        .put(`/api/customers/${customerId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'Updated Name',
          status: 'inactive'
        })
        .expect(200)

      expect(updateResponse.body.data.name).toBe('Updated Name')
      expect(updateResponse.body.data.status).toBe('inactive')
      expect(updateResponse.body.data.email).toBe('original@test.com')
    })

    it('should return 404 for non-existent customer', async () => {
      await request(app)
        .put('/api/customers/99999')
        .set('Authorization', `Bearer ${authToken}`)
        .send({ name: 'Updated' })
        .expect(404)
    })
  })

  describe('DELETE /api/customers/:id', () => {
    it('should soft delete customer', async () => {
      // Create customer
      const createResponse = await request(app)
        .post('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          name: 'To Delete',
          email: 'delete@test.com'
        })
        .expect(201)

      const customerId = createResponse.body.data.id

      // Delete customer
      await request(app)
        .delete(`/api/customers/${customerId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200)

      // Verify soft delete
      const dbCustomer = await db('customers')
        .where('id', customerId)
        .first()

      expect(dbCustomer.deleted_at).toBeTruthy()

      // Should not appear in list
      const listResponse = await request(app)
        .get('/api/customers')
        .set('Authorization', `Bearer ${authToken}`)
        .expect(200)

      const found = listResponse.body.data.find(
        (c: any) => c.id === customerId
      )
      expect(found).toBeUndefined()
    })
  })
})

Database Integration Testing

Database Test Setup

// tests/integration/db/setup.ts
import knex from 'knex'
import config from '~/knexfile'

const testConfig = {
  ...config.test,
  migrations: {
    directory: './migrations'
  },
  seeds: {
    directory: './tests/seeds'
  }
}

export const db = knex(testConfig)

export async function setupTestDb() {
  await db.migrate.rollback()
  await db.migrate.latest()
  await db.seed.run()
}

export async function teardownTestDb() {
  await db.migrate.rollback()
  await db.destroy()
}

Transaction Testing

// tests/integration/db/transactions.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { db, setupTestDb, teardownTestDb } from './setup'
import { CustomerService } from '~/services/CustomerService'

describe('Database Transactions', () => {
  let service: CustomerService

  beforeEach(async () => {
    await setupTestDb()
    service = new CustomerService(db)
  })

  afterEach(async () => {
    await teardownTestDb()
  })

  it('should rollback transaction on error', async () => {
    const trx = await db.transaction()

    try {
      // Insert customer
      const [customerId] = await trx('customers').insert({
        name: 'Test Customer',
        email: 'test@example.com'
      })

      // Insert related data
      await trx('contacts').insert({
        customer_id: customerId,
        name: 'Test Contact',
        email: 'contact@example.com'
      })

      // Simulate error
      throw new Error('Simulated error')

      await trx.commit()
    } catch (error) {
      await trx.rollback()
    }

    // Verify rollback
    const customers = await db('customers')
      .where('email', 'test@example.com')
    const contacts = await db('contacts')
      .where('email', 'contact@example.com')

    expect(customers).toHaveLength(0)
    expect(contacts).toHaveLength(0)
  })

  it('should handle concurrent transactions', async () => {
    const promises = Array.from({ length: 10 }, (_, i) => 
      service.createCustomer({
        name: `Customer ${i}`,
        email: `customer${i}@test.com`
      })
    )

    const results = await Promise.all(promises)

    expect(results).toHaveLength(10)
    results.forEach((customer, index) => {
      expect(customer.name).toBe(`Customer ${index}`)
    })

    const count = await db('customers').count('* as total')
    expect(count[0].total).toBe(10)
  })

  it('should enforce foreign key constraints', async () => {
    await expect(
      db('contacts').insert({
        customer_id: 99999,  // Non-existent customer
        name: 'Test Contact',
        email: 'contact@test.com'
      })
    ).rejects.toThrow(/foreign key constraint/i)
  })

  it('should handle deadlocks gracefully', async () => {
    const customer1 = await service.createCustomer({
      name: 'Customer 1',
      email: 'c1@test.com'
    })
    const customer2 = await service.createCustomer({
      name: 'Customer 2',
      email: 'c2@test.com'
    })

    // Simulate potential deadlock scenario
    const trx1 = await db.transaction()
    const trx2 = await db.transaction()

    try {
      // Transaction 1: Update customer1 then customer2
      await trx1('customers')
        .where('id', customer1.id)
        .update({ status: 'active' })

      // Transaction 2: Update customer2 then customer1
      await trx2('customers')
        .where('id', customer2.id)
        .update({ status: 'active' })

      await trx1('customers')
        .where('id', customer2.id)
        .update({ status: 'inactive' })

      await trx2('customers')
        .where('id', customer1.id)
        .update({ status: 'inactive' })

      await Promise.all([trx1.commit(), trx2.commit()])
    } catch (error) {
      await trx1.rollback()
      await trx2.rollback()
      expect(error.message).toMatch(/deadlock|timeout/i)
    }
  })
})

Service Integration Testing

Multi-Service Testing

// tests/integration/services/workflow.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { CustomerService } from '~/services/CustomerService'
import { EmailService } from '~/services/EmailService'
import { NotificationService } from '~/services/NotificationService'
import { QueueService } from '~/services/QueueService'

describe('Customer Onboarding Workflow', () => {
  let customerService: CustomerService
  let emailService: EmailService
  let notificationService: NotificationService
  let queueService: QueueService

  beforeEach(() => {
    customerService = new CustomerService()
    emailService = new EmailService()
    notificationService = new NotificationService()
    queueService = new QueueService()

    vi.spyOn(emailService, 'send')
    vi.spyOn(notificationService, 'notify')
    vi.spyOn(queueService, 'publish')
  })

  it('should complete full onboarding workflow', async () => {
    // Step 1: Create customer
    const customer = await customerService.create({
      name: 'New Company',
      email: 'new@company.com',
      plan: 'enterprise'
    })

    expect(customer).toHaveProperty('id')
    expect(customer.status).toBe('pending')

    // Step 2: Verify welcome email sent
    expect(emailService.send).toHaveBeenCalledWith({
      to: 'new@company.com',
      template: 'welcome',
      data: expect.objectContaining({
        name: 'New Company'
      })
    })

    // Step 3: Verify internal notification
    expect(notificationService.notify).toHaveBeenCalledWith({
      type: 'new_customer',
      data: expect.objectContaining({
        customerId: customer.id
      })
    })

    // Step 4: Verify queue message
    expect(queueService.publish).toHaveBeenCalledWith(
      'customer.created',
      expect.objectContaining({
        customerId: customer.id,
        plan: 'enterprise'
      })
    )

    // Step 5: Activate customer
    const activated = await customerService.activate(customer.id)
    expect(activated.status).toBe('active')

    // Step 6: Verify activation email
    expect(emailService.send).toHaveBeenCalledWith({
      to: 'new@company.com',
      template: 'activation',
      data: expect.any(Object)
    })

    // Step 7: Check audit trail
    const auditLogs = await customerService.getAuditLogs(customer.id)
    expect(auditLogs).toContainEqual(
      expect.objectContaining({
        action: 'created',
        userId: expect.any(Number)
      })
    )
    expect(auditLogs).toContainEqual(
      expect.objectContaining({
        action: 'activated',
        userId: expect.any(Number)
      })
    )
  })

  it('should handle workflow errors gracefully', async () => {
    // Simulate email service failure
    emailService.send = vi.fn().mockRejectedValue(
      new Error('Email service down')
    )

    const customer = await customerService.create({
      name: 'Test Company',
      email: 'test@company.com'
    })

    // Customer should still be created
    expect(customer).toHaveProperty('id')

    // But status should indicate email failure
    expect(customer.emailVerified).toBe(false)

    // Error should be logged
    const logs = await customerService.getErrorLogs(customer.id)
    expect(logs).toContainEqual(
      expect.objectContaining({
        error: 'Email service down',
        context: 'welcome_email'
      })
    )

    // Retry queue should have the task
    const retryTasks = await queueService.getRetryQueue()
    expect(retryTasks).toContainEqual(
      expect.objectContaining({
        type: 'send_welcome_email',
        customerId: customer.id
      })
    )
  })
})

Third-Party Integration Testing

External API Mocking

// tests/integration/external/payment.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import nock from 'nock'
import { PaymentService } from '~/services/PaymentService'

describe('Payment Gateway Integration', () => {
  let paymentService: PaymentService

  beforeEach(() => {
    paymentService = new PaymentService({
      apiKey: 'test_key',
      baseUrl: 'https://api.payment.com'
    })

    nock.cleanAll()
  })

  it('should process payment successfully', async () => {
    // Mock payment API response
    nock('https://api.payment.com')
      .post('/charges')
      .reply(200, {
        id: 'ch_123',
        status: 'succeeded',
        amount: 9999,
        currency: 'usd'
      })

    const result = await paymentService.charge({
      amount: 9999,
      currency: 'usd',
      source: 'tok_visa',
      description: 'CRM Subscription'
    })

    expect(result.status).toBe('succeeded')
    expect(result.id).toBe('ch_123')
  })

  it('should handle payment failures', async () => {
    nock('https://api.payment.com')
      .post('/charges')
      .reply(402, {
        error: {
          code: 'card_declined',
          message: 'Your card was declined'
        }
      })

    await expect(
      paymentService.charge({
        amount: 9999,
        source: 'tok_declined'
      })
    ).rejects.toThrow('Your card was declined')
  })

  it('should retry on network errors', async () => {
    // First attempt fails
    nock('https://api.payment.com')
      .post('/charges')
      .replyWithError('Network error')

    // Second attempt fails
    nock('https://api.payment.com')
      .post('/charges')
      .replyWithError('Network error')

    // Third attempt succeeds
    nock('https://api.payment.com')
      .post('/charges')
      .reply(200, {
        id: 'ch_123',
        status: 'succeeded'
      })

    const result = await paymentService.chargeWithRetry({
      amount: 9999,
      source: 'tok_visa'
    })

    expect(result.status).toBe('succeeded')
    expect(result.attempts).toBe(3)
  })

  it('should handle webhook validation', async () => {
    const payload = {
      id: 'evt_123',
      type: 'charge.succeeded',
      data: {
        object: {
          id: 'ch_123',
          amount: 9999
        }
      }
    }

    const signature = paymentService.generateWebhookSignature(
      JSON.stringify(payload)
    )

    const isValid = await paymentService.validateWebhook(
      payload,
      signature
    )

    expect(isValid).toBe(true)
  })
})

Message Queue Integration

Queue Testing

// tests/integration/queue/message.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import amqp from 'amqplib'
import { QueueService } from '~/services/QueueService'
import { EmailWorker } from '~/workers/EmailWorker'

describe('Message Queue Integration', () => {
  let connection: amqp.Connection
  let channel: amqp.Channel
  let queueService: QueueService
  let emailWorker: EmailWorker

  beforeEach(async () => {
    connection = await amqp.connect('amqp://localhost')
    channel = await connection.createChannel()

    queueService = new QueueService(channel)
    emailWorker = new EmailWorker(channel)

    await channel.assertQueue('test_queue', { durable: true })
  })

  afterEach(async () => {
    await channel.deleteQueue('test_queue')
    await channel.close()
    await connection.close()
  })

  it('should publish and consume messages', async () => {
    const messages: any[] = []

    // Set up consumer
    await emailWorker.consume('test_queue', async (msg) => {
      messages.push(JSON.parse(msg.content.toString()))
      channel.ack(msg)
    })

    // Publish messages
    await queueService.publish('test_queue', {
      type: 'welcome_email',
      customerId: 123
    })

    await queueService.publish('test_queue', {
      type: 'password_reset',
      userId: 456
    })

    // Wait for processing
    await new Promise(resolve => setTimeout(resolve, 100))

    expect(messages).toHaveLength(2)
    expect(messages[0].type).toBe('welcome_email')
    expect(messages[1].type).toBe('password_reset')
  })

  it('should handle message acknowledgment', async () => {
    let processedCount = 0

    await emailWorker.consume('test_queue', async (msg) => {
      const data = JSON.parse(msg.content.toString())

      if (data.shouldFail) {
        // Reject and requeue
        channel.nack(msg, false, true)
      } else {
        processedCount++
        channel.ack(msg)
      }
    })

    // Send messages
    await queueService.publish('test_queue', { shouldFail: true })
    await queueService.publish('test_queue', { shouldFail: false })

    await new Promise(resolve => setTimeout(resolve, 100))

    expect(processedCount).toBe(1)

    // Check queue for requeued message
    const msg = await channel.get('test_queue')
    expect(msg).toBeTruthy()
    if (msg) {
      const data = JSON.parse(msg.content.toString())
      expect(data.shouldFail).toBe(true)
    }
  })

  it('should handle dead letter queue', async () => {
    // Set up DLQ
    await channel.assertExchange('dlx', 'direct')
    await channel.assertQueue('dlq')
    await channel.bindQueue('dlq', 'dlx', '')

    // Main queue with DLX
    await channel.assertQueue('main_queue', {
      arguments: {
        'x-dead-letter-exchange': 'dlx',
        'x-max-retries': 3
      }
    })

    let attempts = 0
    await emailWorker.consume('main_queue', async (msg) => {
      attempts++
      // Always reject to test DLQ
      channel.nack(msg, false, false)
    })

    await queueService.publish('main_queue', { test: 'data' })

    await new Promise(resolve => setTimeout(resolve, 200))

    // Message should be in DLQ after max retries
    const dlqMsg = await channel.get('dlq')
    expect(dlqMsg).toBeTruthy()
    if (dlqMsg) {
      const data = JSON.parse(dlqMsg.content.toString())
      expect(data.test).toBe('data')
    }
  })
})

Cache Integration Testing

Redis Cache Testing

// tests/integration/cache/redis.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import Redis from 'ioredis'
import { CacheService } from '~/services/CacheService'

describe('Redis Cache Integration', () => {
  let redis: Redis
  let cache: CacheService

  beforeEach(async () => {
    redis = new Redis({
      host: 'localhost',
      port: 6379,
      db: 1  // Use separate DB for tests
    })

    cache = new CacheService(redis)
    await redis.flushdb()
  })

  afterEach(async () => {
    await redis.quit()
  })

  it('should cache and retrieve data', async () => {
    const data = { id: 1, name: 'Test Customer' }

    await cache.set('customer:1', data, 60)
    const cached = await cache.get('customer:1')

    expect(cached).toEqual(data)
  })

  it('should expire cache after TTL', async () => {
    await cache.set('temp:1', 'data', 1)  // 1 second TTL

    // Should exist immediately
    let value = await cache.get('temp:1')
    expect(value).toBe('data')

    // Wait for expiration
    await new Promise(resolve => setTimeout(resolve, 1100))

    value = await cache.get('temp:1')
    expect(value).toBeNull()
  })

  it('should handle cache stampede', async () => {
    let dbCalls = 0
    const fetchFromDb = async () => {
      dbCalls++
      await new Promise(resolve => setTimeout(resolve, 100))
      return { data: 'from db' }
    }

    // Simulate multiple concurrent requests
    const promises = Array.from({ length: 10 }, () =>
      cache.getOrSet('shared:key', fetchFromDb, 60)
    )

    const results = await Promise.all(promises)

    // All should get same data
    results.forEach(result => {
      expect(result).toEqual({ data: 'from db' })
    })

    // DB should only be called once
    expect(dbCalls).toBe(1)
  })

  it('should handle cache invalidation', async () => {
    // Set multiple related keys
    await cache.set('customer:1', { name: 'Customer 1' })
    await cache.set('customer:2', { name: 'Customer 2' })
    await cache.set('customer:list', ['1', '2'])

    // Invalidate pattern
    await cache.invalidatePattern('customer:*')

    // All should be gone
    expect(await cache.get('customer:1')).toBeNull()
    expect(await cache.get('customer:2')).toBeNull()
    expect(await cache.get('customer:list')).toBeNull()
  })
})

File Upload Integration

File Upload Testing

// tests/integration/upload/file.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import request from 'supertest'
import fs from 'fs/promises'
import path from 'path'
import { app } from '~/app'

describe('File Upload Integration', () => {
  const uploadDir = path.join(__dirname, 'test-uploads')

  beforeEach(async () => {
    await fs.mkdir(uploadDir, { recursive: true })
  })

  afterEach(async () => {
    await fs.rm(uploadDir, { recursive: true, force: true })
  })

  it('should upload single file', async () => {
    const response = await request(app)
      .post('/api/upload')
      .attach('file', path.join(__dirname, 'fixtures/test.pdf'))
      .expect(200)

    expect(response.body).toHaveProperty('filename')
    expect(response.body).toHaveProperty('size')
    expect(response.body).toHaveProperty('mimetype')
    expect(response.body.mimetype).toBe('application/pdf')

    // Verify file exists
    const uploadedFile = path.join(uploadDir, response.body.filename)
    const stats = await fs.stat(uploadedFile)
    expect(stats.size).toBeGreaterThan(0)
  })

  it('should validate file type', async () => {
    const response = await request(app)
      .post('/api/upload')
      .attach('file', path.join(__dirname, 'fixtures/test.exe'))
      .expect(422)

    expect(response.body.error).toContain('Invalid file type')
  })

  it('should enforce file size limit', async () => {
    // Create large file
    const largeFile = path.join(uploadDir, 'large.txt')
    await fs.writeFile(largeFile, Buffer.alloc(11 * 1024 * 1024))  // 11MB

    const response = await request(app)
      .post('/api/upload')
      .attach('file', largeFile)
      .expect(422)

    expect(response.body.error).toContain('File too large')
  })

  it('should handle multiple file uploads', async () => {
    const response = await request(app)
      .post('/api/upload/multiple')
      .attach('files', path.join(__dirname, 'fixtures/doc1.pdf'))
      .attach('files', path.join(__dirname, 'fixtures/doc2.pdf'))
      .attach('files', path.join(__dirname, 'fixtures/image.png'))
      .expect(200)

    expect(response.body.files).toHaveLength(3)
    response.body.files.forEach((file: any) => {
      expect(file).toHaveProperty('filename')
      expect(file).toHaveProperty('size')
    })
  })

  it('should scan uploaded files for viruses', async () => {
    const response = await request(app)
      .post('/api/upload/secure')
      .attach('file', path.join(__dirname, 'fixtures/test.pdf'))
      .expect(200)

    expect(response.body.scanned).toBe(true)
    expect(response.body.clean).toBe(true)
  })
})

Performance Testing

Load Testing

// tests/integration/performance/load.test.ts
import { describe, it, expect } from 'vitest'
import autocannon from 'autocannon'
import { app } from '~/app'

describe('Load Testing', () => {
  it('should handle concurrent requests', async () => {
    const server = app.listen(0)
    const port = server.address().port

    const result = await autocannon({
      url: `http://localhost:${port}/api/customers`,
      connections: 10,
      pipelining: 1,
      duration: 10,
      headers: {
        'Authorization': 'Bearer test-token'
      }
    })

    server.close()

    expect(result.errors).toBe(0)
    expect(result.timeouts).toBe(0)
    expect(result.requests.average).toBeGreaterThan(100)  // 100+ req/sec
    expect(result.latency.p99).toBeLessThan(1000)  // 99% under 1 second
  })

  it('should handle database connection pool', async () => {
    const promises = Array.from({ length: 100 }, (_, i) =>
      db.raw('SELECT SLEEP(0.1) as result')
    )

    const start = Date.now()
    await Promise.all(promises)
    const duration = Date.now() - start

    // Should use connection pool efficiently
    expect(duration).toBeLessThan(5000)  // Should complete in under 5 seconds
  })
})

End-to-End Testing

Complete User Journey

// tests/integration/e2e/customer-journey.test.ts
import { describe, it, expect } from 'vitest'
import { Browser, Page } from 'playwright'
import { chromium } from 'playwright'

describe('Customer Journey E2E', () => {
  let browser: Browser
  let page: Page

  beforeAll(async () => {
    browser = await chromium.launch()
  })

  afterAll(async () => {
    await browser.close()
  })

  beforeEach(async () => {
    page = await browser.newPage()
  })

  afterEach(async () => {
    await page.close()
  })

  it('should complete full customer signup flow', async () => {
    // Navigate to signup
    await page.goto('http://localhost:3000/signup')

    // Fill signup form
    await page.fill('[name="company"]', 'Test Company')
    await page.fill('[name="email"]', 'test@company.com')
    await page.fill('[name="password"]', 'SecurePass123!')
    await page.click('[type="submit"]')

    // Should redirect to verification
    await page.waitForURL('**/verify-email')
    expect(page.url()).toContain('/verify-email')

    // Simulate email verification (in test mode)
    await page.goto('http://localhost:3000/verify?token=test-token')

    // Should redirect to dashboard
    await page.waitForURL('**/dashboard')
    expect(page.url()).toContain('/dashboard')

    // Should see welcome message
    const welcome = await page.textContent('.welcome-message')
    expect(welcome).toContain('Welcome, Test Company')

    // Create first customer
    await page.click('[data-testid="add-customer"]')
    await page.fill('[name="customerName"]', 'First Customer')
    await page.fill('[name="customerEmail"]', 'customer@example.com')
    await page.click('[data-testid="save-customer"]')

    // Should appear in list
    await page.waitForSelector('[data-testid="customer-list"]')
    const customerName = await page.textContent(
      '[data-testid="customer-list"] li:first-child'
    )
    expect(customerName).toContain('First Customer')
  })
})

Test Environment Configuration

Docker Compose for Testing

# docker-compose.test.yml
version: '3.8'

services:
  mysql-test:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: test
      MYSQL_DATABASE: crm_test
    ports:
      - "3307:3306"
    tmpfs:
      - /var/lib/mysql

  redis-test:
    image: redis:7-alpine
    ports:
      - "6380:6379"

  rabbitmq-test:
    image: rabbitmq:3-management
    ports:
      - "5673:5672"
      - "15673:15672"
    environment:
      RABBITMQ_DEFAULT_USER: test
      RABBITMQ_DEFAULT_PASS: test

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"

Last Updated: January 2025 Version: 1.0.0