Testing Strategy - ThriveSend B2B2G
Testing Strategy - ThriveSend B2B2G
Section titled “Testing Strategy - ThriveSend B2B2G”Last Updated: January 26, 2026 Status: Production Developer Guide: Testing
Overview
Section titled “Overview”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
Testing Philosophy
Section titled “Testing Philosophy”B2B2G Testing Principles
Section titled “B2B2G Testing Principles”- Compliance First: Every feature must have POPIA compliance tests
- Multi-Tenant Isolation: Test organization scoping in all data operations
- Security Clearance: Validate government security requirements
- Sector-Specific: Test both government and business workflows
- Audit Logging: Verify audit trails for all sensitive operations
Test Pyramid
Section titled “Test Pyramid”graph TD E2E[E2E Tests - UI Flows] --> Integration[Integration Tests - Full Workflows] Integration --> Unit[Unit Tests - Functions & Logic] Unit --> Static[Static Analysis - TypeScript, ESLint]| Layer | Purpose | Count | Tools |
|---|---|---|---|
| E2E Tests | Full user journeys | Planned | Playwright |
| Integration Tests | Multi-component workflows | 5 files | Jest + Prisma mocks |
| API Tests | Endpoint validation | 12 files | Jest + Next.js mocks |
| Unit Tests | Business logic, utilities | 8 files | Jest |
| Static Analysis | Type safety, linting | Continuous | TypeScript, ESLint |
Running Tests
Section titled “Running Tests”All Tests
Section titled “All Tests”# Run all testspnpm test
# Run with coveragepnpm test:coverage
# Run in watch mode (development)pnpm test:watchSpecific Test Suites
Section titled “Specific Test Suites”# Run only unit testspnpm test src/lib
# Run only integration testspnpm test src/__tests__/integration
# Run only compliance testspnpm test src/__tests__/compliance
# Run only API testspnpm test src/app/api
# Run a specific test filepnpm test src/__tests__/compliance/popia-compliance.test.tsE2E Tests (Playwright)
Section titled “E2E Tests (Playwright)”# Run E2E testspnpm test:e2e
# Run E2E tests in UI modepnpm test:e2e:uiTest Structure
Section titled “Test Structure”Test File Organization
Section titled “Test File Organization”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.tsTest File Naming Convention
Section titled “Test File Naming Convention”# Unit tests: *.test.tsutils.test.tsvalidation.test.ts
# Integration tests: *.integration.test.tsservice-provider-onboarding.integration.test.tsclient-management-flow.integration.test.ts
# E2E tests: *.e2e.test.ts (Playwright)onboarding-flow.e2e.test.tsUnit Testing
Section titled “Unit Testing”Basic Test Structure
Section titled “Basic Test Structure”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') })})Real Example: POPIA Compliance Validation
Section titled “Real Example: POPIA Compliance Validation”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 Testing
Section titled “Integration Testing”Integration Test Pattern
Section titled “Integration Test Pattern”Integration tests verify complete workflows across multiple components.
Real Example: Service Provider Onboarding
Section titled “Real Example: Service Provider Onboarding”From src/__tests__/integration/01-service-provider-onboarding.integration.test.ts:
import { prisma } from '@/lib/db'import { auth } from '@clerk/nextjs/server'
// Mock dependenciesjest.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
API Testing
Section titled “API Testing”API Test Pattern
Section titled “API Test Pattern”// Mock Next.js request/responseconst 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 authenticationjest.mock('@clerk/nextjs/server', () => ({ auth: jest.fn().mockResolvedValue({ userId: 'test-user-id', orgId: 'test-org-id', }),}))Real Example: Invitations API Test
Section titled “Real Example: Invitations API Test”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
Mocking Patterns
Section titled “Mocking Patterns”Mocking Prisma
Section titled “Mocking Prisma”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 testimport { prisma } from '@/lib/db'
;(prisma.client.findMany as jest.Mock).mockResolvedValue([ { id: 'client-1', name: 'Test Client' }])Mocking Clerk Authentication
Section titled “Mocking Clerk Authentication”jest.mock('@clerk/nextjs/server', () => ({ auth: jest.fn().mockResolvedValue({ userId: 'user_123', orgId: 'org_456', }),}))
// Test unauthorizedimport { auth } from '@clerk/nextjs/server';(auth as jest.Mock).mockResolvedValue({ userId: null, orgId: null })Mocking Next.js API Routes
Section titled “Mocking Next.js API Routes”import { NextRequest, NextResponse } from 'next/server'
// Mock requestconst 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 responseconst jsonSpy = jest.spyOn(NextResponse, 'json')Test Coverage
Section titled “Test Coverage”Viewing Coverage Reports
Section titled “Viewing Coverage Reports”# Generate coverage reportpnpm test:coverage
# Open HTML coverage reportopen coverage/index.htmlCoverage Goals
Section titled “Coverage Goals”| Category | Target | Current |
|---|---|---|
| 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)
Writing Good Tests
Section titled “Writing Good Tests”Test Best Practices
Section titled “Test Best Practices”-
Use descriptive test names:
// ✅ GOODit('should create government client with security clearance BASIC')it('should reject invitation without POPIA consent')// ❌ BADit('test client creation')it('test invitation') -
Follow AAA pattern (Arrange, Act, Assert):
it('should validate organization scoping', () => {// Arrangeconst organizationId = 'org_123'const mockClients = [{ id: '1', organizationId }]// Act;(prisma.client.findMany as jest.Mock).mockResolvedValue(mockClients)const result = await getClients(organizationId)// Assertexpect(result).toHaveLength(1)expect(result[0].organizationId).toBe(organizationId)}) -
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)}) -
Clean up after tests:
beforeEach(() => {jest.clearAllMocks()})afterEach(() => {jest.restoreAllMocks()})
❌ DON’T
Section titled “❌ DON’T”-
Test implementation details:
// ❌ BAD - Testing internal stateexpect(component.state.isLoading).toBe(false)// ✅ GOOD - Testing behaviorexpect(screen.queryByTestId('loading')).not.toBeInTheDocument() -
Write flaky tests:
// ❌ BAD - Timing-dependentsetTimeout(() => expect(result).toBe('done'), 1000)// ✅ GOOD - Wait for promiseawait waitFor(() => expect(result).toBe('done')) -
Skip POPIA compliance tests:
// ❌ NEVER skip compliance testsit.skip('should log audit trail')// ✅ Always include compliance validationit('should log audit trail for client creation', async () => {await createClient(data)expect(prisma.auditLog.create).toHaveBeenCalledWith({data: expect.objectContaining({action: 'CLIENT_CREATED',resourceType: 'Client'})})})
Continuous Integration
Section titled “Continuous Integration”GitHub Actions Workflow
Section titled “GitHub Actions Workflow”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.jsonRelated Documentation:
- Service Architecture - Testing services
- API Development - Testing API routes
Last Updated: January 26, 2026