Skip to content

Testing Strategy - ThriveSend B2B2G

Last Updated: January 26, 2026 Status: Production Developer Guide: Testing


This guide covers the comprehensive testing strategy for ThriveSend B2B2G, including unit testing, integration testing, API testing, compliance testing, and E2E testing.

Testing Stack:

  • Unit Tests: Jest with TypeScript
  • Integration Tests: Jest with mocked Prisma and Clerk
  • API Tests: Jest with Next.js request/response mocks
  • E2E Tests: Playwright (mentioned for future implementation)
  • Compliance Tests: POPIA-specific validation suites

Current Test Coverage:

  • 25 test files implemented
  • Compliance testing: POPIA, security clearance, government workflows
  • Integration testing: Service provider onboarding, client management, campaigns
  • API testing: Endpoints, validation, authorization

  1. Compliance First: Every feature must have POPIA compliance tests
  2. Multi-Tenant Isolation: Test organization scoping in all data operations
  3. Security Clearance: Validate government security requirements
  4. Sector-Specific: Test both government and business workflows
  5. Audit Logging: Verify audit trails for all sensitive operations
graph TD
E2E[E2E Tests - UI Flows] --> Integration[Integration Tests - Full Workflows]
Integration --> Unit[Unit Tests - Functions & Logic]
Unit --> Static[Static Analysis - TypeScript, ESLint]
LayerPurposeCountTools
E2E TestsFull user journeysPlannedPlaywright
Integration TestsMulti-component workflows5 filesJest + Prisma mocks
API TestsEndpoint validation12 filesJest + Next.js mocks
Unit TestsBusiness logic, utilities8 filesJest
Static AnalysisType safety, lintingContinuousTypeScript, ESLint

Terminal window
# Run all tests
pnpm test
# Run with coverage
pnpm test:coverage
# Run in watch mode (development)
pnpm test:watch
Terminal window
# Run only unit tests
pnpm test src/lib
# Run only integration tests
pnpm test src/__tests__/integration
# Run only compliance tests
pnpm test src/__tests__/compliance
# Run only API tests
pnpm test src/app/api
# Run a specific test file
pnpm test src/__tests__/compliance/popia-compliance.test.ts
Terminal window
# Run E2E tests
pnpm test:e2e
# Run E2E tests in UI mode
pnpm test:e2e:ui

src/
├── __tests__/ # Main test directory
│ ├── setup.test.ts # Test configuration
│ ├── compliance/ # POPIA compliance tests (4 files)
│ │ ├── popia-compliance.test.ts
│ │ ├── popia-compliance-validation.test.ts
│ │ ├── government-clearance.test.ts
│ │ └── security-compliance.test.ts
│ ├── integration/ # Integration tests (5 files)
│ │ ├── 01-service-provider-onboarding.integration.test.ts
│ │ ├── 02-client-management-flow.integration.test.ts
│ │ ├── 03-campaign-execution-flow.integration.test.ts
│ │ ├── 04-popia-audit-trail.integration.test.ts
│ │ └── 05-security-clearance-workflow.integration.test.ts
│ └── api/ # API endpoint tests
│ └── invitations-api.test.ts
├── app/api/ # API route tests (12 files)
│ └── [endpoint]/__tests__/
│ └── route.test.ts
└── lib/services/__tests__/ # Service layer tests
├── analytics.test.ts
└── invitation-security.test.ts
Terminal window
# Unit tests: *.test.ts
utils.test.ts
validation.test.ts
# Integration tests: *.integration.test.ts
service-provider-onboarding.integration.test.ts
client-management-flow.integration.test.ts
# E2E tests: *.e2e.test.ts (Playwright)
onboarding-flow.e2e.test.ts

import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'
describe('Feature Name', () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
jest.restoreAllMocks()
})
it('should perform expected behavior', () => {
// Arrange
const input = 'test data'
// Act
const result = functionUnderTest(input)
// Assert
expect(result).toBe('expected output')
})
})

From src/__tests__/compliance/popia-compliance.test.ts:

describe('POPIA Compliance Test Suite', () => {
describe('Data Processing Lawfulness', () => {
it('should validate consent-based data processing', () => {
const validateDataProcessing = (data: {
consentGiven: boolean
purposeSpecified: boolean
dataMinimized: boolean
}) => {
const { consentGiven, purposeSpecified, dataMinimized } = data
if (!consentGiven) {
return { compliant: false, error: 'POPIA requires explicit consent for data processing' }
}
if (!purposeSpecified) {
return { compliant: false, error: 'POPIA requires specific purpose for data collection' }
}
if (!dataMinimized) {
return { compliant: false, error: 'POPIA requires data minimization - collect only necessary data' }
}
return { compliant: true }
}
// Test compliant case
const compliantCase = validateDataProcessing({
consentGiven: true,
purposeSpecified: true,
dataMinimized: true
})
expect(compliantCase.compliant).toBe(true)
// Test no consent case
const noConsentCase = validateDataProcessing({
consentGiven: false,
purposeSpecified: true,
dataMinimized: true
})
expect(noConsentCase.compliant).toBe(false)
expect(noConsentCase.error).toBe('POPIA requires explicit consent for data processing')
})
it('should validate data subject rights implementation', () => {
const dataSubjectRights = {
ACCESS: 'right to access personal information',
CORRECTION: 'right to correct personal information',
DELETION: 'right to delete personal information',
OBJECTION: 'right to object to processing',
PORTABILITY: 'right to data portability'
}
const validateDataSubjectRights = (requestType: string, userRole: string) => {
const validRights = Object.keys(dataSubjectRights)
if (!validRights.includes(requestType)) {
return { valid: false, error: 'Invalid data subject right request' }
}
// Service providers must honor all rights for their clients
if (userRole === 'SERVICE_PROVIDER_ADMIN') {
return { valid: true, action: `Process ${requestType} request` }
}
// Users can exercise rights on their own data
if (userRole === 'DATA_SUBJECT') {
return { valid: true, action: `Allow ${requestType} for user's own data` }
}
return { valid: false, error: 'Insufficient permissions for data subject rights request' }
}
expect(validateDataSubjectRights('ACCESS', 'SERVICE_PROVIDER_ADMIN').valid).toBe(true)
expect(validateDataSubjectRights('DELETION', 'DATA_SUBJECT').valid).toBe(true)
expect(validateDataSubjectRights('CORRECTION', 'CONTENT_CREATOR').valid).toBe(false)
expect(validateDataSubjectRights('INVALID_RIGHT', 'SERVICE_PROVIDER_ADMIN').valid).toBe(false)
})
})
})

What This Tests:

  • POPIA consent requirements
  • Data processing lawfulness
  • Data subject rights (access, correction, deletion)
  • Role-based permissions for POPIA compliance

Integration tests verify complete workflows across multiple components.

From src/__tests__/integration/01-service-provider-onboarding.integration.test.ts:

import { prisma } from '@/lib/db'
import { auth } from '@clerk/nextjs/server'
// Mock dependencies
jest.mock('@/lib/db', () => ({
prisma: {
organization: {
create: jest.fn(),
findUnique: jest.fn(),
},
subscription: {
create: jest.fn(),
},
organizationUser: {
create: jest.fn(),
},
securityConfiguration: {
create: jest.fn(),
},
complianceConfiguration: {
create: jest.fn(),
},
auditLog: {
create: jest.fn(),
},
},
}))
jest.mock('@clerk/nextjs/server')
describe('Integration: Service Provider Onboarding Flow', () => {
const mockClerkOrgId = 'org_clerk_12345'
const mockUserId = 'user_clerk_67890'
beforeEach(() => {
jest.clearAllMocks()
;(auth as jest.Mock).mockResolvedValue({
userId: mockUserId,
orgId: mockClerkOrgId,
})
})
it('should complete full onboarding flow for new service provider', async () => {
// Step 1: Create organization in database (synced with Clerk)
const newOrganization = {
id: 'org-db-001',
clerkOrganizationId: mockClerkOrgId,
name: 'Test Marketing Agency',
slug: 'test-marketing-agency',
type: 'MARKETING_AGENCY',
subscription: 'PROFESSIONAL',
dataResidency: 'SOUTH_AFRICA',
popiaCompliant: true,
createdAt: new Date(),
updatedAt: new Date(),
}
;(prisma.organization.create as jest.Mock).mockResolvedValue(newOrganization)
// Step 2: Create default Professional subscription
const defaultSubscription = {
id: 'sub-001',
organizationId: 'org-db-001',
planId: 'professional',
planName: 'Professional',
planType: 'PROFESSIONAL',
status: 'ACTIVE',
price: 550000, // R5,500 in cents
currency: 'ZAR',
interval: 'MONTHLY',
subscribedAt: new Date(),
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // +30 days
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
}
;(prisma.subscription.create as jest.Mock).mockResolvedValue(defaultSubscription)
// Step 3: Create initial admin user
const adminUser = {
id: 'user-db-001',
organizationId: 'org-db-001',
clerkUserId: mockUserId,
email: 'admin@testmarketing.co.za',
firstName: 'Admin',
lastName: 'User',
role: 'SERVICE_PROVIDER_ADMIN',
permissions: ['FULL_ACCESS'],
status: 'ACTIVE',
createdAt: new Date(),
updatedAt: new Date(),
}
;(prisma.organizationUser.create as jest.Mock).mockResolvedValue(adminUser)
// Step 4: Create default security configuration
const securityConfig = {
id: 'sec-config-001',
organizationId: 'org-db-001',
enforceIPWhitelist: false,
enforceMFA: false,
sessionTimeout: 3600,
passwordMinLength: 8,
passwordRequireSpecialChar: true,
passwordRequireNumber: true,
passwordRequireUppercase: true,
createdAt: new Date(),
updatedAt: new Date(),
}
;(prisma.securityConfiguration.create as jest.Mock).mockResolvedValue(securityConfig)
// Verify all steps executed in order
expect(prisma.organization.create).toHaveBeenCalledTimes(1)
expect(prisma.subscription.create).toHaveBeenCalledTimes(1)
expect(prisma.organizationUser.create).toHaveBeenCalledTimes(1)
expect(prisma.securityConfiguration.create).toHaveBeenCalledTimes(1)
// Verify data flow
expect(newOrganization.clerkOrganizationId).toBe(mockClerkOrgId)
expect(defaultSubscription.organizationId).toBe(newOrganization.id)
expect(adminUser.organizationId).toBe(newOrganization.id)
expect(securityConfig.organizationId).toBe(newOrganization.id)
})
})

What This Tests:

  • Complete onboarding workflow
  • Database transaction sequence
  • Clerk-to-database synchronization
  • Default configuration creation
  • Multi-tenant organization setup
  • POPIA compliance initialization

// Mock Next.js request/response
const mockNextRequest = (body?: unknown, method: string = 'POST') => {
return {
json: jest.fn().mockResolvedValue(body),
method,
url: 'http://localhost:3000/api/endpoint',
headers: {
get: jest.fn().mockReturnValue('application/json'),
},
}
}
const mockNextResponse = () => {
const response = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
}
return {
NextResponse: {
json: jest.fn().mockImplementation((data, options) => ({
...response,
data,
status: options?.status || 200,
})),
},
}
}
// Mock Clerk authentication
jest.mock('@clerk/nextjs/server', () => ({
auth: jest.fn().mockResolvedValue({
userId: 'test-user-id',
orgId: 'test-org-id',
}),
}))

From src/__tests__/api/invitations-api.test.ts:

describe('Invitation API Integration Tests', () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
jest.restoreAllMocks()
})
describe('POST /api/invitations/simple', () => {
it('should validate B2B2G invitation request structure', async () => {
const validInvitationRequest = {
email: 'user@company.co.za',
role: 'CONTENT_CREATOR',
securityClearance: 'PUBLIC',
customMessage: 'Welcome to our B2B2G team!'
}
const validateInvitationRequest = (data: any) => {
const errors: string[] = []
if (!data.email) {
errors.push('Email is required')
} else if (!data.email.includes('@')) {
errors.push('Invalid email format')
}
if (!data.role) {
errors.push('Role is required')
}
if (!data.securityClearance) {
errors.push('Security clearance is required')
}
return {
valid: errors.length === 0,
errors
}
}
const validation = validateInvitationRequest(validInvitationRequest)
expect(validation.valid).toBe(true)
expect(validation.errors).toHaveLength(0)
const invalidRequest = validateInvitationRequest({})
expect(invalidRequest.valid).toBe(false)
expect(invalidRequest.errors).toContain('Email is required')
expect(invalidRequest.errors).toContain('Role is required')
expect(invalidRequest.errors).toContain('Security clearance is required')
})
it('should enforce South African email domain requirements for government roles', async () => {
const validateGovernmentRoleEmail = (email: string, role: string) => {
const governmentRoles = ['GOVERNMENT_LIAISON', 'COMPLIANCE_OFFICER']
const isGovernmentRole = governmentRoles.includes(role)
const hasGovDomain = email.includes('.gov.za')
if (isGovernmentRole && !hasGovDomain) {
return {
valid: false,
error: 'Government roles require .gov.za email domain'
}
}
return { valid: true }
}
const validGov = validateGovernmentRoleEmail('user@municipality.gov.za', 'GOVERNMENT_LIAISON')
expect(validGov.valid).toBe(true)
const invalidGov = validateGovernmentRoleEmail('user@company.co.za', 'GOVERNMENT_LIAISON')
expect(invalidGov.valid).toBe(false)
expect(invalidGov.error).toBe('Government roles require .gov.za email domain')
})
})
})

What This Tests:

  • API request validation
  • B2B2G-specific business rules
  • Government email domain requirements
  • Security clearance validation
  • Error handling and responses

jest.mock('@/lib/db', () => ({
prisma: {
client: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
campaign: {
findMany: jest.fn(),
create: jest.fn(),
},
auditLog: {
create: jest.fn(),
},
},
}))
// In test
import { prisma } from '@/lib/db'
;(prisma.client.findMany as jest.Mock).mockResolvedValue([
{ id: 'client-1', name: 'Test Client' }
])
jest.mock('@clerk/nextjs/server', () => ({
auth: jest.fn().mockResolvedValue({
userId: 'user_123',
orgId: 'org_456',
}),
}))
// Test unauthorized
import { auth } from '@clerk/nextjs/server'
;(auth as jest.Mock).mockResolvedValue({ userId: null, orgId: null })
import { NextRequest, NextResponse } from 'next/server'
// Mock request
const mockRequest = {
json: jest.fn().mockResolvedValue({ data: 'test' }),
headers: {
get: jest.fn().mockReturnValue('application/json')
},
nextUrl: {
searchParams: new URLSearchParams('?param=value')
}
} as unknown as NextRequest
// Mock response
const jsonSpy = jest.spyOn(NextResponse, 'json')

Terminal window
# Generate coverage report
pnpm test:coverage
# Open HTML coverage report
open coverage/index.html
CategoryTargetCurrent
Statements> 80%75%
Branches> 70%68%
Functions> 80%72%
Lines> 80%74%

Priority Areas for Coverage:

  • ✅ POPIA compliance logic (95%+)
  • ✅ Security clearance validation (90%+)
  • ✅ Multi-tenant isolation (90%+)
  • 🔄 Analytics services (65% - needs improvement)
  • 🔄 Content approval workflows (60% - needs improvement)

  1. Use descriptive test names:

    // ✅ GOOD
    it('should create government client with security clearance BASIC')
    it('should reject invitation without POPIA consent')
    // ❌ BAD
    it('test client creation')
    it('test invitation')
  2. Follow AAA pattern (Arrange, Act, Assert):

    it('should validate organization scoping', () => {
    // Arrange
    const organizationId = 'org_123'
    const mockClients = [{ id: '1', organizationId }]
    // Act
    ;(prisma.client.findMany as jest.Mock).mockResolvedValue(mockClients)
    const result = await getClients(organizationId)
    // Assert
    expect(result).toHaveLength(1)
    expect(result[0].organizationId).toBe(organizationId)
    })
  3. Test edge cases and errors:

    it('should handle unauthorized access', async () => {
    ;(auth as jest.Mock).mockResolvedValue({ userId: null })
    const response = await GET(mockRequest)
    expect(response.status).toBe(401)
    })
  4. Clean up after tests:

    beforeEach(() => {
    jest.clearAllMocks()
    })
    afterEach(() => {
    jest.restoreAllMocks()
    })
  1. Test implementation details:

    // ❌ BAD - Testing internal state
    expect(component.state.isLoading).toBe(false)
    // ✅ GOOD - Testing behavior
    expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
  2. Write flaky tests:

    // ❌ BAD - Timing-dependent
    setTimeout(() => expect(result).toBe('done'), 1000)
    // ✅ GOOD - Wait for promise
    await waitFor(() => expect(result).toBe('done'))
  3. Skip POPIA compliance tests:

    // ❌ NEVER skip compliance tests
    it.skip('should log audit trail')
    // ✅ Always include compliance validation
    it('should log audit trail for client creation', async () => {
    await createClient(data)
    expect(prisma.auditLog.create).toHaveBeenCalledWith({
    data: expect.objectContaining({
    action: 'CLIENT_CREATED',
    resourceType: 'Client'
    })
    })
    })

.github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Run linter
run: pnpm lint
- name: Run type check
run: pnpm type-check
- name: Run tests
run: pnpm test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json

Related Documentation:


Last Updated: January 26, 2026