Skip to content

Testing

The Activation System uses Vitest as its test framework across all packages in the monorepo. Each package has its own vitest.config.ts and runs tests independently via pnpm --filter. Tests cover route integration, service unit tests, Zod schema validation, database encryption, RLS policies, and queue worker logic.

Running Tests

Per-package commands

Each workspace package runs its own test suite. Use pnpm --filter to target a specific package:

CommandPackageTest Files
pnpm --filter @activation-sys/api testAPI (Hono)apps/api/tests/**/*.test.ts (35 files)
pnpm --filter @activation-sys/database testDatabasepackages/database/src/__tests__/**/*.test.ts (8 files)
pnpm --filter @activation-sys/queue testQueuepackages/queue/tests/**/*.test.ts (4 files)
pnpm --filter @activation-sys/shared testSharedpackages/shared/src/__tests__/**/*.test.ts (5 files)

Watch mode

To run tests in watch mode during development (re-runs on file changes):

bash
# API watch mode
pnpm --filter @activation-sys/api test:watch

# Database watch mode
pnpm --filter @activation-sys/database test:watch

# Queue watch mode
pnpm --filter @activation-sys/queue test:watch

# Shared watch mode
pnpm --filter @activation-sys/shared test:watch

Run all tests

There is no root-level test script. Run all packages individually:

bash
pnpm --filter @activation-sys/api test && \
pnpm --filter @activation-sys/database test && \
pnpm --filter @activation-sys/queue test && \
pnpm --filter @activation-sys/shared test

Prerequisites

Before running tests, ensure typecheck and lint pass:

bash
pnpm check    # turbo typecheck across all packages
pnpm lint     # biome check .

Test Framework and Setup

Framework: Vitest (^3.0.0 in API/shared, ^3.2.4 in database, ^4.1.4 in queue)

Configuration: Each package defines its own vitest.config.ts with package-specific settings:

API config (apps/api/vitest.config.ts):

json
{
  "test": {
    "globals": true,
    "testTimeout": 10000,
    "include": ["tests/**/*.test.ts"]
  },
  "resolve": {
    "alias": {
      "@": "<project_root>/apps/api/src",
      "@activation-sys/shared": "<project_root>/packages/shared/src",
      "@activation-sys/database": "<project_root>/packages/database/src"
    }
  }
}

Shared config (packages/shared/vitest.config.ts):

json
{
  "test": {
    "globals": true,
    "testTimeout": 10000,
    "include": ["src/__tests__/**/*.test.ts"]
  }
}

Queue config (packages/queue/vitest.config.ts):

json
{
  "test": {
    "testTimeout": 30000,
    "hookTimeout": 30000,
    "teardownTimeout": 15000
  }
}

The queue package uses extended timeouts because Redis integration tests need additional time for connection and cleanup.

Database package: No vitest.config.ts file — uses Vitest defaults with the src/__tests__/ glob convention.

Global test setup: globals: true in API and shared configs means describe, it, expect, vi are available without explicit imports.

Test Categories

Route/Integration Tests (apps/api/tests/)

These tests exercise Hono HTTP endpoints using the app.request() test client. They verify request validation, middleware behavior, response shapes, and bilingual (Arabic/English) output:

Test FileArea
auth.test.tsRegistration, login, OTP verification, logout, profile, phone change (phone + email)
auth-middleware.test.tsAuth middleware — JWT verification, role checks, public path handling
email-auth-middleware.test.tsEmail-based auth middleware
email-auth-service.test.tsEmail auth service logic
operators.test.tsOperator CRUD and listing endpoints
packages.test.tsPackage listing and detail endpoints
search.test.tsPackage search endpoint
availability.test.tsPackage availability checks
sync-packages.test.tsPackage sync endpoint
activations.test.tsSIM activation endpoints
activation-service.test.tsActivation business logic
payments.test.tsPayment intent and processing endpoints
payment-service.test.tsPayment service logic
payment-provider.test.tsPayment provider abstraction
stripe-webhook.test.tsStripe webhook handler
rate-limit.test.tsRate limiting middleware
health.test.tsHealth check endpoint
cold-start.test.tsServer startup + live health check
error-handler.test.tsError handler middleware
env-validation.test.tsEnvironment variable Zod validation
notification-service.test.tsNotification delivery
otp-service.test.tsOTP generation and verification
session-service.test.tsSession management
sms-provider.test.tsSMS provider abstraction
email-provider.test.tsEmail provider abstraction
operator-plugin.test.tsOperator plugin system
profile-routes.test.tsUser profile endpoints
profile-compliance.test.tsProfile data compliance (PDPL)
compliance-retention.test.tsData retention and compliance
status-tracking.test.tsActivation status tracking
realtime-types.test.tsRealtime event type definitions

Behavioral Tests (apps/api/tests/*-behavioral.test.ts)

These tests validate domain-specific behaviors — what the system should do from a user perspective, not just how individual endpoints respond:

Test FileValidates
search-behavioral.test.tsSearch with q param returns results, pg_trgm fallback, filters apply within search scope, requires q param, matchedBy indicator
packages-behavioral.test.tsBilingual package responses, filter behavior, promotional flags
sync-packages-behavioral.test.tsSync behavior — idempotency, delta handling
availability-behavioral.test.tsAvailability checks for packages and operators
operators-behavioral.test.tsFilters isActive=true, returns bilingual nameEn/nameAr, excludes sensitive apiConfig/apiBaseUrl

Schema Validation Tests (packages/shared/src/__tests__/)

Zod schema validation tests ensure request/response schemas accept valid data and reject invalid data:

Test FileValidates
schemas.test.tsUser, operator, package, activation, payment, ticket schemas — accepts valid data, rejects invalid formats, enforces bilingual fields
errors.test.tsAppError hierarchy — AuthError (401/403), ValidationError (400), NotFoundError (404), ActivationError (422), PaymentError (402/409), OperatorError (502/504), toApiResponse bilingual output
payment-contracts.test.tsPayment intent, STC Pay, Apple Pay, Stripe webhook schemas and TypeScript types
status-profile-contracts.test.tsActivation history query, profile update, profile export, delete profile schemas
email-auth.test.tsEmail authentication schema validation

Database Schema Tests (packages/database/src/__tests__/)

These tests verify database schema correctness, encryption, and Row Level Security:

Test FileValidates
encryption.test.tsAES-256-GCM encrypt/decrypt round-trip, random IV uniqueness, tamper detection, unicode/Arabic text handling, empty string
rls-policies.test.ts17 tables have RLS enabled, CREATE POLICY statements present, covers 5 roles (end_user, b2b_admin, activation_officer, super_admin, support), audit_logs immutability trigger
schema.test.tsTable structure, column types, relationships
tls-config.test.tsTLS configuration for database connections
catalog-schema.test.tsCatalog (operators/packages) schema structure
payment-schema.test.tsPayment table schema
email-hash-schema.test.tsEmail hash storage schema
status-events-schema.test.tsActivation status events schema

Queue Worker Tests (packages/queue/tests/)

Test FileValidates
queue-unit.test.tsRETRY_CONFIG (activation: 3 attempts, payment: 5, notification: 2), QUEUE_NAMES, activationBackoff delay calculation, queue instances with correct names, redis createConnection/disconnectall without real Redis
queue.test.tsQueue integration behavior
payment-worker.test.tsprocessPaymentJobsettle_payment enqueues activation, duplicate handling, reconcile_failure skips activation, missing data throws for BullMQ retry, operator slug from DB not hardcoded
payment-settlement.test.tsPayment settlement worker logic

Test Patterns

Hono route test using app.request()

The Hono test client allows sending HTTP requests directly to the app instance without starting a server:

typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Hono } from 'hono';

function createTestApp(): Hono {
  const app = new Hono();
  app.onError((err, c) => {
    // Error handler converts AppError subclasses to HTTP responses
    if (err instanceof AuthError) {
      return c.json({ error: { code: err.code, messageEn: err.messageEn, messageAr: err.messageAr } }, 401);
    }
    return c.json({ error: { code: 'INTERNAL_ERROR', message: err.message } }, 500);
  });
  app.use('*', languageMiddleware);
  app.use('*', authMiddleware);
  app.post('/auth/register', validate(registerSchema), async (c) => {
    // route handler
  });
  return app;
}

describe('Auth Routes', () => {
  it('should return 201 with valid registration data', async () => {
    const app = createTestApp();
    const res = await app.request('/auth/register', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept-Language': 'ar-SA',
      },
      body: JSON.stringify({
        phone: '+966501234567',
        name: 'مستخدم تجريبي',
        consents: [
          { type: 'terms_of_service', version: '1.0' },
          { type: 'privacy_policy', version: '1.0' },
          { type: 'data_processing', version: '1.0' },
        ],
      }),
    });

    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.message).toBe('تم إرسال رمز التحقق'); // Arabic response
  });
});

Key points:

  • Mock external dependencies (redis, database, supabase) at the module level using vi.mock()
  • Use vi.hoisted() for mock objects referenced in vi.mock() factory functions
  • Import modules after mock declarations
  • Test bilingual responses by setting Accept-Language header

Zod schema validation test

typescript
import { describe, it, expect } from 'vitest';
import { createUserSchema, packageSchema } from '../schemas/index.js';

describe('Zod Schema Validation', () => {
  it('createUserSchema accepts valid user data', () => {
    expect(() => createUserSchema.parse({
      phone: '0501234567',
      email: 'user@example.com',
      name: 'Ali',
      role: 'end_user',
      identityType: 'citizen',
    })).not.toThrow();
  });

  it('createUserSchema rejects invalid role', () => {
    expect(() => createUserSchema.parse({
      phone: '0501234567',
      email: 'user@example.com',
      name: 'Ali',
      role: 'hacker', // not in enum
      identityType: 'citizen',
    })).toThrow();
  });

  it('packageSchema requires bilingual name fields', () => {
    const validPackage = {
      id: '550e8400-e29b-41d4-a716-446655440000',
      nameEn: 'Test Package',
      nameAr: 'باقة تجريبية',
      // ... other required fields
    };
    expect(() => packageSchema.parse(validPackage)).not.toThrow();
  });
});

Key points:

  • Use .parse() to test both acceptance and rejection
  • Test boundary conditions: empty strings, negative numbers, wrong enums, missing required fields
  • Verify bilingual fields (nameEn/nameAr) are required where applicable

Database encryption round-trip test

typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { encrypt, decrypt } from '../utils/encryption.js';

const TEST_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';

describe('encryption', () => {
  beforeEach(() => {
    process.env.ENCRYPTION_KEY = TEST_KEY;
  });

  it('should encrypt plaintext and decrypt back to the original value', () => {
    const plaintext = 'hello world';
    const encrypted = encrypt(plaintext);
    const decrypted = decrypt(encrypted);
    expect(decrypted).toBe(plaintext);
  });

  it('should produce different ciphertext for same plaintext (random IV)', () => {
    const encrypted1 = encrypt('hello');
    const encrypted2 = encrypt('hello');
    expect(encrypted1).not.toBe(encrypted2); // random IV
    expect(decrypt(encrypted1)).toBe('hello');
    expect(decrypt(encrypted2)).toBe('hello');
  });

  it('should throw when decrypting tampered ciphertext (GCM auth tag)', () => {
    const encrypted = encrypt('sensitive data');
    const tampered = encrypted.slice(0, -4) + 'dead';
    expect(() => decrypt(tampered)).toThrow();
  });

  it('should handle unicode/Arabic text', () => {
    const plaintext = 'مرحبا بالعالم';
    const encrypted = encrypt(plaintext);
    expect(decrypt(encrypted)).toBe(plaintext);
  });
});

Key points:

  • Set ENCRYPTION_KEY environment variable in beforeEach
  • Test AES-256-GCM properties: random IV, authentication tag, unicode support
  • Arabic text encryption is critical for PDPL-compliant PII storage

Queue worker test with mock Redis

typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock BullMQ to avoid real Redis connections
vi.mock('bullmq', () => ({
  Worker: vi.fn().mockImplementation((_name, processor, _opts) => {
    return { on: vi.fn(), close: vi.fn() };
  }),
  Queue: vi.fn().mockImplementation(() => ({
    add: vi.fn().mockResolvedValue({ id: 'job-1' }),
    on: vi.fn(),
  })),
}));

describe('Payment Worker', () => {
  beforeEach(() => {
    vi.resetModules();
    vi.clearAllMocks();
  });

  it('settle_payment enqueues activation job for completed payment', async () => {
    const { processPaymentJob } = await import('../src/workers/payment-worker.js');
    const mockDb = {
      queryPayments: vi.fn().mockResolvedValue([{ id: 'pay-1', status: 'completed' }]),
      queryOrderItems: vi.fn().mockResolvedValue([{ id: 'oi-1', orderId: 'ord-1' }]),
      queryActivations: vi.fn().mockResolvedValue([{ id: 'act-1', operatorId: 'op-1' }]),
      queryOperator: vi.fn().mockResolvedValue({ id: 'op-1', slug: 'stc' }),
    };

    const result = await processPaymentJob({
      action: 'settle_payment',
      paymentId: 'pay-1',
    }, mockDb);

    expect(result.success).toBe(true);
  });
});

Key points:

  • Mock bullmq module entirely — no Redis connection needed
  • Use vi.resetModules() in beforeEach for dynamic import() tests
  • Pass mock database objects to worker functions for isolated testing
  • Worker tests verify retry behavior (throws on missing data) and idempotency

Environment Handling

Tests run with NODE_ENV=test. The environment schema in apps/api/src/config/env.ts uses Zod with sensible defaults:

NODE_ENV: z.enum(['development', 'production', 'test']).default('development')
PORT: z.coerce.number().default(3001)
SMS_PROVIDER: z.enum(['mock', 'twilio', 'taqnyat']).default('mock')
JWT_EXPIRY_SECONDS: z.coerce.number().default(3600)

No real Supabase or Redis required for unit tests. All external dependencies are mocked:

  • Redis — mocked via vi.mock('../src/config/redis.js') with stub methods (exists, set, get, incr, del, expire, eval)
  • Supabase — mocked via vi.mock('@supabase/supabase-js') with createClient returning stub auth.admin methods
  • Database (Drizzle) — mocked via vi.mock('../src/config/database.js') with chainable select/insert/update stubs
  • BullMQ — mocked via vi.mock('bullmq') with stub Worker and Queue constructors

Queue tests skip gracefully when Redis is unavailable — the mock prevents any real connection attempts. The queue package's vitest.config.ts uses longer timeouts (testTimeout: 30000) to accommodate tests that may need connection setup time.

CI Integration

No CI/CD pipeline detected in the repository (no .github/workflows/ directory). Before tests can reliably run in CI, the following pre-checks should pass locally:

  1. Typecheck: pnpm check — runs turbo run typecheck across all packages
  2. Lint: pnpm lint — runs biome check .

Recommended CI workflow order:

  1. pnpm install --frozen-lockfile
  2. pnpm check (typecheck)
  3. pnpm lint (biome)
  4. Run test suites per package
  5. pnpm --filter @activation-sys/api test
  6. pnpm --filter @activation-sys/database test
  7. pnpm --filter @activation-sys/queue test
  8. pnpm --filter @activation-sys/shared test

Coverage

Run coverage reports with the --coverage flag:

bash
pnpm --filter @activation-sys/api exec vitest run --coverage
pnpm --filter @activation-sys/database exec vitest run --coverage
pnpm --filter @activation-sys/queue exec vitest run --coverage
pnpm --filter @activation-sys/shared exec vitest run --coverage

No coverage threshold is configured in any vitest.config.ts file. Coverage is informational only.

Example coverage report output:

 % Coverage report from v8
 ----------------------------------------------------
 File                    | Lines | Branches | Funcs | Stmts |
 ------------------------|-------|----------|-------|-------|
 src/middleware/auth.ts   |  85.7 |    75.0  | 100.0 |  87.5 |
 src/routes/health.ts     | 100.0 |   100.0  | 100.0 | 100.0 |
 src/routes/operators.ts  |  92.3 |    88.8  | 100.0 |  93.1 |
 ------------------------|-------|----------|-------|-------|
 All files                |  91.2 |    84.1  | 100.0 |  92.0 |

Coverage configuration can be added to any vitest.config.ts:

json
{
  "test": {
    "coverage": {
      "provider": "v8",
      "reporter": ["text", "html", "lcov"],
      "include": ["src/**/*.ts"],
      "exclude": ["src/**/__tests__/**", "src/types/**"]
    }
  }
}

Writing New Tests

File naming convention:

PackageLocationPattern
APIapps/api/tests/*.test.ts
Databasepackages/database/src/__tests__/*.test.ts
Queuepackages/queue/tests/*.test.ts
Sharedpackages/shared/src/__tests__/*.test.ts

No shared test helpers directory exists — each test file sets up its own mocks inline. When adding a new test:

  1. Place the test file in the correct location per the table above
  2. Follow the existing mock patterns for the package (see Test Patterns)
  3. Use describe/it blocks with descriptive names (English, not Arabic)
  4. For route tests, use app.request() — never start a real HTTP server (except cold-start.test.ts which explicitly tests server startup)
  5. For behavioral tests, name the file <feature>-behavioral.test.ts and test domain requirements
  6. For encryption/security tests, set the required env vars in beforeEach

Frontend Checks

The new frontend workspaces currently have type/build checks but no behavior test suites yet:

WorkspaceCheck commandNotes
apps/web-b2cpnpm --filter @activation-sys/web-b2c typecheckNext.js B2C web shell and bilingual content modules
apps/sitepnpm --filter @activation-sys/site typecheckVite landing site shell and bilingual content modules

When meaningful customer behavior is added, place tests near the changed frontend code and cover validation, auth state handling, localization parity, and API failure paths where applicable.

Internal documentation - Activation System