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
pnpm --filter @activation-sys/database testDatabasepackages/database/src/__tests__/**/*.test.ts
pnpm --filter @activation-sys/queue testQueuepackages/queue/tests/**/*.test.ts
pnpm --filter @activation-sys/shared testSharedpackages/shared/src/__tests__/**/*.test.ts

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
moyasar.test.tsMoyasar adapter, credentials, mode selection and disabled production fallback
moyasar-settlement.test.tsSettlement, original-ID method switching, checkout locking
moyasar-reconciliation.test.tsServer-side verification and failure paths
payment-return.test.tsPublic neutral return page and protected confirmation boundary
moyasar-routes.test.tsMoyasar webhook handler
paymob.test.tsPaymob contracts, payment provider factory, client, adapter, settings and callback authentication
paymob-routes.test.tsPaymob webhook handler, checkout admin routes and Paymob confirmation
paymob-reconciliation.test.tsPaymob server-side verification and failure paths
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-orchestration-service.test.tsNotification creation, delivery handoff, reconciliation, and audit behavior
notification-audience-service.test.tsRecipient chunking and company-admin membership selection
notification-batch-service.test.tsBatch dispatch, scheduling, cancellation, and delivery counts
notification-routes.test.tsAuthenticated inbox, ownership, pagination, read/seen/archive, and opt-outs
admin-notification-routes.test.tsSuper-admin guards, acceptance, idempotent replay, compliance filtering, and batch reads
notification-webhooks.test.tsSignature checks, receipt deduplication, enqueue failure recovery, and outcome normalization
notification-dispatch.test.tsProvider selection, mock-only notification SMS, and unsupported capability handling
notification-provider-callback.test.tsMock callback signature verification and normalized parsing
notification-templates.test.tsRegistry rendering, bilingual output, and content validation
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.tsLegacy shared payment schemas/types; current Moyasar and Paymob API contracts are covered by API tests
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 policies (activation: 3 attempts, payment: 5, notification email: 5, SMS: 3, orchestration: 5), queue names/instances, notification retention/priority/namespaces, and activation backoff, with mocked BullMQ and Redis configuration
queue.test.tsQueue integration behavior
notification-queues.test.tsDeterministic IDs, retained-failure retry races, bulk channel grouping, priorities, scheduling, and handoff sweep registration
notification-channel-worker.test.tsPost-claim recipient gates, claim fencing, retries, terminal outcomes, provider dispatch, metrics, and bounded shutdown
notification-orchestration-worker.test.tsBatch/sweep/callback dispatch, concurrency, metrics, and bounded shutdown
notification-queue-lifecycle.test.tsClosing all three producers and supplied clients on success, failure, and timeout
notification-worker-lifecycle.test.tsForced cleanup of supplied and blocking worker connections
payment-worker.test.tsprocessPaymentJob — settle_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
payment-reconciliation.test.tspayment-reconciliation durable handoff, deterministic job IDs (Moyasar IDs stable across the rename), retries/retention, and worker processing

Notification Verification ​

The retired queue and service tests are replaced by the notification suites above. Run the queue and API package test commands from this guide. Within the API suite, also check startup-workers.test.ts, smtp-email-provider.test.ts, activation-notifications.test.ts, notification-esim-spike.test.ts, profile-compliance.test.ts, and compliance-retention.test.ts for wiring, SMTP deadlines, attachment preservation, export, and content scrubbing.

The orchestration service suite uses mocked persistence and enqueue functions to check handoff selection SQL and crash-boundary behavior. Those checks do not prove concurrent Postgres locking. Likewise, preserving an SMTP attachment in a mock does not prove live mail receipt or exactly-once delivery.

When changing notification behavior, cover invalid input, ownership and role denial, successful persistence and dispatch, provider rejection, and retry exhaustion. Include account deactivation or opt-out after enqueue, stale claim recovery, an old attempt finishing after a replacement, enqueue failure after persistence, cancellation racing a batch start, and callback arrival before the provider message ID is saved. Distinguish finished batch dispatch from successful channel outcomes, and keep addresses and rendered content out of queue assertions.

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