Skip to content

Integration Testing Guide

Comprehensive guide for the E2E Integration Testing Framework for the Microlearning Platform API.

Table of Contents


Testing Philosophy

Core Principles

  1. Isolation: Tests run in a dedicated local environment with a clean database reset before each test run, ensuring no interference between test runs.

  2. Deterministic State: The database is reset before each test run, applying all migrations including seed data. This eliminates flaky tests caused by data mutations from previous runs.

  3. Real Infrastructure: Tests run against actual Supabase Edge Functions via supabase functions serve, matching the production deployment structure as closely as possible.

  4. Known Test Data: Tests use seed data from SQL migrations (20241229000002_seed_agentic_ai_curriculum.sql) providing predictable, known IDs for assertions.

  5. Comprehensive Coverage: All 36+ API endpoints are tested with multiple scenarios including success cases, error handling, authentication, and authorization.

What We Test

Category Description
Functional Each endpoint returns correct data with proper schema
Authentication Protected endpoints require valid JWT tokens
Authorization Users can only access their own data
Pagination List endpoints support pagination parameters
Filtering Query parameters filter results correctly
Error Handling Invalid requests return appropriate error codes
Schema Validation Responses match expected Zod schemas

What We Don't Test

  • Unit tests for individual functions (separate concern)
  • UI/Frontend integration
  • Performance/load testing
  • Database migration logic

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         TEST RUNNER                                  │
│                    (Deno Test + @std/testing/bdd)                   │
└────────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                       TEST CLIENT                                    │
│                  (HTTP requests + Auth + Coverage)                   │
└────────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                    SUPABASE FUNCTIONS SERVE                          │
│                      (Port 54321 - Local Instance)                     │
├─────────────────────────────────────────────────────────────────────┤
│  graph  │  content  │  home  │  search  │  metadata  │  user  │ ... │
└────────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│                      POSTGRESQL DATABASE                             │
│                      (Port 54322 - Local Instance)                     │
│                                                                      │
│   Seed Data: Agentic AI Curriculum                                   │
│   - 1 Domain, 3 Trails, 12 Concepts, 18 Sparks, 18 Beacons          │
└─────────────────────────────────────────────────────────────────────┘

Port Allocation

Service Port
API 54321
Database 54322
Studio 54323
Inbucket 54324
SMTP 54325
POP3 54326

Project Structure

tests/
├── scripts/                      # IT Environment Management
│   ├── start-it-env.ts           # Start isolated Supabase instance
│   ├── stop-it-env.ts            # Stop local environment
│   ├── reset-it-db.ts            # Reset database with migrations
│   └── seed-test-users.ts        # Create test users with subscriptions
├── e2e/
│   ├── setup/                    # Test Infrastructure
│   │   ├── global-setup.ts       # Initialize test context, auth users
│   │   ├── test-client.ts        # HTTP client with auth & coverage
│   │   ├── test-context.ts       # Shared state (tokens, config)
│   │   └── auth-helpers.ts       # Authentication utilities
│   │
│   ├── fixtures/                 # Test Data
│   │   ├── users.ts              # Test user credentials per role
│   │   ├── content.ts            # Reference IDs from seed migration
│   │   └── (test data fixtures)
│   │
│   ├── utils/                    # Test Utilities
│   │   ├── assertions.ts         # Custom assertion helpers
│   │   └── schema-validator.ts   # Zod schema validation
│   │
│   ├── suites/                   # Test Suites by Function
│   │   ├── graph/                # Graph API tests (9 files)
│   │   │   ├── domains.test.ts
│   │   │   ├── trails.test.ts
│   │   │   ├── concepts.test.ts
│   │   │   ├── sparks.test.ts
│   │   │   ├── beacons.test.ts
│   │   │   ├── explore.test.ts
│   │   │   ├── path.test.ts
│   │   │   ├── breadcrumb.test.ts
│   │   │   └── links.test.ts
│   │   ├── content/
│   │   │   └── sparks.test.ts
│   │   ├── home/
│   │   │   └── home.test.ts
│   │   ├── search/
│   │   │   ├── search.test.ts
│   │   │   └── autocomplete.test.ts
│   │   ├── metadata/
│   │   │   └── metadata.test.ts
│   │   ├── snapshots/
│   │   │   └── snapshots.test.ts
│   │   └── user/
│   │       ├── profile.test.ts
│   │       ├── stats.test.ts
│   │       ├── milestones.test.ts
│   │       ├── journeys.test.ts
│   │       └── events.test.ts
│   │
│   └── protection/               # Security Tests
│       ├── authentication.test.ts
│       └── authorization.test.ts

supabase/
└── config.toml                   # Local environment config

.env.local                           # Local environment variables
.github/workflows/
└── integration-tests.yml         # CI/CD workflow

Local Environment Setup

Prerequisites

  • Deno v1.x installed
  • Supabase CLI installed (npm install -g supabase)
  • Docker running (for Supabase local)

Configuration Files

.env.local - Environment Variables

# Local Supabase URLs
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres

# Local Supabase Keys (default local development keys)
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# Test Configuration
TEST_BASE_URL=http://127.0.0.1:54321/functions/v1
TEST_TIMEOUT_MS=30000
TEST_VERBOSE=false

supabase/config.toml - Supabase Configuration

The local configuration defines the standard ports:

[api]
port = 54321

[db]
port = 54322

[studio]
port = 54323

Starting the Local Environment

# Recommended: Using deno task
deno task local:start

The start script:

  1. Checks if local environment is already running
  2. Starts Supabase with the local configuration
  3. Waits for services to be ready
  4. Verifies health of API, Database, and Auth services

Stopping the Local Environment

deno task local:stop

Resetting the Database

deno task local:reset

This command:

  1. Runs supabase db reset which drops all data
  2. Re-applies all migrations in order
  3. Seeds the curriculum data from 20241229000002_seed_agentic_ai_curriculum.sql
  4. Verifies seed data is present

Seeding Test Users

deno task local:seed

Creates test users with different subscription tiers:

Role Email Subscription
FREE test-free@example.com free
PRO test-pro@example.com pro
PREMIUM test-premium@example.com premium
ADMIN test-admin@example.com premium

Also creates:

  • Test journeys (trail progress)
  • Test learning events
  • Test milestones

Complete Setup

# One command to reset and seed
deno task local:setup

Serving Functions

For development and debugging of edge functions in the local environment:

# Serve all functions with local environment variables
deno task function:serve

# Serve with verbose logging
deno task function:serve:verbose

What this does:

  1. Verifies local environment is running on port 54321
  2. Serves edge functions with hot-reload using .env.local
  3. Functions are accessible at http://127.0.0.1:54321/functions/v1/{function-name}

Note: For running integration tests, you don't typically need to serve functions separately. The deno task local:start command starts Supabase which already serves the functions at http://127.0.0.1:54321/functions/v1/. The function:serve task is useful for:

  • Hot-reload during function development
  • Debugging function code
  • Testing function changes without restarting Supabase

Test Infrastructure

TestClient

The TestClient class (tests/e2e/setup/test-client.ts) provides:

const client = new TestClient(baseUrl, token?);

// HTTP methods
await client.get("/graph/domains");
await client.post("/me/events", { spark_id: "...", verb: "started" });
await client.put("/resource", data);
await client.patch("/resource", data);
await client.delete("/resource");

// Schema validation
client.validateSchema(response.data, DomainListResponseSchema);

// Coverage tracking
const testedEndpoints = getTestedEndpoints();

Test Context

The TestContext (tests/e2e/setup/test-context.ts) provides:

interface TestContext {
  config: {
    supabaseUrl: string;
    supabaseAnonKey: string;
    supabaseServiceKey: string;
    testBaseUrl: string;
  };
  tokens: Map<UserRole, string>;
  users: Map<UserRole, TestUser>;
  supabase: SupabaseClient;
}

Auth Helpers

Authentication utilities (tests/e2e/setup/auth-helpers.ts):

// Authenticate a user
const { token, userId } = await authenticateUser(email, password);

// Get token for a role
const token = await getTokenForRole(UserRole.PRO);

// Authenticate all test users
const tokens = await authenticateTestUsers();

Fixtures

Users (tests/e2e/fixtures/users.ts)

import { getUserByRole, TEST_USER_CREDENTIALS } from "../fixtures/users.ts";

const proUser = getUserByRole("pro");
// { email: "test-pro@example.com", password: "test123456", role: UserRole.PRO }

Content (tests/e2e/fixtures/content.ts)

import {
  getSparkBySlug,
  TEST_DOMAIN,
  TEST_SPARKS,
  TEST_TRAILS,
} from "../fixtures/content.ts";

// Known IDs from seed data
TEST_DOMAIN.id; // "11111111-1111-1111-1111-111111111111"
TEST_DOMAIN.slug; // "agentic-ai"

const spark = getSparkBySlug("what-is-an-ai-agent");

Assertions

Custom assertions (tests/e2e/utils/assertions.ts):

import {
  assertDataMetaResponse, // { data, meta } structure
  assertDomainEntity, // domain-specific fields
  assertEntityFields, // id, slug, timestamps
  assertForbidden, // 403
  assertNotFound, // 404
  assertPaginationMeta, // valid pagination
  assertSlug, // valid slug format
  assertStatus, // specific status
  assertSuccess, // 2xx status
  assertUnauthorized, // 401
  assertUUID, // valid UUID format
} from "../utils/assertions.ts";

Schema Validation

Zod schemas (tests/e2e/utils/schema-validator.ts):

import {
  assertSchemaValid,
  DomainListResponseSchema,
  DomainSchema,
  SparkSchema,
  TrailSchema,
} from "../utils/schema-validator.ts";

// Validate response
assertSchemaValid(DomainListResponseSchema, response.data, "Domain list");

Writing Tests

Test File Structure

/**
 * Graph API - Domains Endpoint Tests
 */

import { beforeAll, describe, it } from "@std/testing/bdd";
import {
  createAnonymousClient,
  globalSetup,
  TestClient,
} from "../../setup/global-setup.ts";
import { TEST_DOMAIN } from "../../fixtures/content.ts";
import { assertNotFound, assertSuccess } from "../../utils/assertions.ts";

describe("Graph API - Domains", () => {
  let client: TestClient;

  beforeAll(async () => {
    await globalSetup();
    client = createAnonymousClient();
  });

  describe("GET /graph/domains", () => {
    it("should list domains without authentication", async () => {
      const response = await client.get("/graph/domains");
      assertSuccess(response);
    });

    it("should return 404 for non-existent domain", async () => {
      const response = await client.get("/graph/domains/non-existent");
      assertNotFound(response);
    });
  });
});

Test Patterns

Public Endpoint Test

it("should list domains without authentication", async () => {
  const response = await client.get("/graph/domains");

  assertSuccess(response);
  assertDataMetaResponse(response);

  const data = response.data as { data: unknown[]; meta: unknown };
  assertMinLength(data.data, 1, "Should have at least one domain");
  assertPaginationMeta(data.meta);
});

Protected Endpoint Test

it("should require authentication", async () => {
  const anonClient = createAnonymousClient();
  const response = await anonClient.get("/user/me");

  assertUnauthorized(response);
});

it("should return profile with valid token", async () => {
  const authClient = await createAuthenticatedClient(UserRole.PRO);
  const response = await authClient.get("/user/me");

  assertSuccess(response);
  assertSchemaValid(UserProfileSchema, response.data.data);
});

Role-Based Test

for (const role of [UserRole.FREE, UserRole.PRO, UserRole.PREMIUM]) {
  it(`should allow ${role} user access`, async () => {
    const client = await createAuthenticatedClient(role);
    const response = await client.get("/user/me");
    assertSuccess(response);
  });
}

Schema Validation Test

it("should validate response schema", async () => {
  const response = await client.get("/graph/domains");

  assertSuccess(response);
  assertSchemaValid(DomainListResponseSchema, response.data, "Domain list");
});

Pagination Test

it("should support pagination", async () => {
  const response = await client.get("/graph/domains?page=1&per_page=5");

  assertSuccess(response);
  const data = response.data as { meta: unknown };
  assertPaginationMeta(data.meta, { page: 1, perPage: 5 });
});

Filter Test

it("should filter by difficulty", async () => {
  const response = await client.get("/graph/trails?difficulty=beginner");

  assertSuccess(response);
  const data = response.data as { data: Array<{ difficulty: string }> };

  for (const trail of data.data) {
    assertEquals(trail.difficulty, "beginner");
  }
});

Adding a New Test File

  1. Create file in appropriate directory:

    tests/e2e/suites/{function}/{endpoint}.test.ts
    

  2. Follow the standard structure with imports from setup/fixtures/utils

  3. Run to verify:

    deno test --allow-all tests/e2e/suites/{function}/{endpoint}.test.ts
    


Running Tests

Commands

Command Description
deno task local:start Start local Supabase environment
deno task local:stop Stop local environment
deno task local:reset Reset database with migrations
deno task local:seed Seed test users
deno task local:setup Reset + seed (before tests)
deno task test:e2e:all Run all E2E tests
deno task test:e2e:graph Run graph API tests only
deno task test:e2e:content Run content API tests only
deno task test:e2e:user Run user API tests only
deno task test:e2e:snapshots Run snapshots tests only
deno task test:e2e:protection Run protection tests only
deno task test:e2e:coverage Run with coverage report
deno task test:e2e:ci Run with JUnit XML output

Complete Test Workflow

# 1. Start local environment (first time or after stopping)
deno task local:start

# 2. Reset database and seed users
deno task local:setup

# 3. Start edge functions (in separate terminal)
deno task function:serve

# 4. Run tests
deno task test:e2e:all

# 5. Stop when done
deno task local:stop

Running Specific Tests

# Single test file
deno test --allow-all tests/e2e/graph/domains.test.ts

# With filter
deno test --allow-all --filter "should list domains" tests/e2e/

# With verbose output
deno test --allow-all --trace-leaks tests/e2e/

Generating Reports

# Coverage report (lcov format)
deno task test:e2e:coverage
# Output: coverage.lcov

# JUnit XML (for CI)
deno task test:e2e:ci
# Output: test-results.xml

CI/CD Integration

GitHub Actions Workflow

The workflow (.github/workflows/integration-tests.yml) runs on:

  • Push to main or develop
  • Pull requests to main

Workflow Steps

  1. Checkout - Get code
  2. Setup Deno - Install Deno runtime
  3. Setup Supabase CLI - Install Supabase CLI
  4. Cache Dependencies - Cache Deno modules
  5. Start IT Environment - Start isolated Supabase
  6. Verify Supabase - Health check
  7. Reset Database - Apply migrations and seed data
  8. Seed Test Users - Create authenticated users
  9. Start Edge Functions - Serve functions in background
  10. Run Tests - Execute test suite with JUnit output
  11. Upload Results - Store test-results.xml artifact
  12. Generate Coverage - Create lcov report
  13. Upload to Codecov - Send coverage report
  14. Publish Results - Comment on PR with results
  15. Cleanup - Stop Supabase

Required Secrets

No secrets required - uses default local Supabase keys.

Artifacts

Artifact Description
test-results.xml JUnit test results
coverage.lcov Code coverage report

Troubleshooting

Common Issues

Local Environment Won't Start

# Check if ports are in use
lsof -i :54321

# Stop any existing Supabase instances
supabase stop --all

# Then start local
deno task local:start

Tests Fail with "Connection Refused"

Ensure edge functions are running:

deno task function:serve

Authentication Failures

Reset and reseed users:

deno task local:setup

Stale Test Data

Reset database:

deno task local:reset

Type Errors in Tests

Check imports and run type check:

deno check tests/e2e/suites/**/*.ts

Debug Mode

Run tests with verbose output:

TEST_VERBOSE=true deno test --allow-all tests/e2e/

Check function logs:

# In separate terminal while functions are serving
deno task logs

Maintenance Guide

Adding New Endpoints

  1. Create test file:

    touch tests/e2e/suites/new/endpoint.test.ts
    

  2. Add test directory to deno.json (if new function):

    "test:e2e:new": "deno test --allow-all tests/e2e/suites/new/"
    

Updating Seed Data

When seed data changes:

  1. Update tests/e2e/fixtures/content.ts with new IDs
  2. Update expected counts in tests
  3. Run full test suite to verify

Adding New Test Users

  1. Update tests/scripts/seed-test-users.ts
  2. Add to tests/e2e/fixtures/users.ts
  3. Add role to UserRole enum if new

Updating Schemas

When API schemas change:

  1. Update schemas in tests/e2e/utils/schema-validator.ts
  2. Run schema validation tests to verify

Quick Reference

Endpoint Coverage

Function Endpoints Test File(s)
graph 21 domains, trails, concepts, sparks, beacons, explore, path, breadcrumb, links
content 3 sparks
home 1 home
search 2 search, autocomplete
metadata 1 metadata
snapshots 3 snapshots
user 8 profile, stats, milestones, journeys, events
Total 39 24 test files

Test User Credentials

Role Email Password
FREE test-free@example.com test123456
PRO test-pro@example.com test123456
PREMIUM test-premium@example.com test123456
ADMIN test-admin@example.com test123456

Seed Data Reference

Entity Count Example Slug
Domains 1 agentic-ai
Trails 3 agentic-ai-foundations
Concepts 12 understanding-ai-agents
Sparks 18 what-is-an-ai-agent
Beacons 18 python, langchain, react

Import Cheatsheet

// BDD test structure
import { beforeAll, describe, it } from "@std/testing/bdd";

// Setup
import {
  createAnonymousClient,
  createAuthenticatedClient,
  globalSetup,
} from "../../setup/global-setup.ts";
import { UserRole } from "../../setup/test-context.ts";

// Fixtures
import {
  getSparkBySlug,
  TEST_DOMAIN,
  TEST_SPARKS,
} from "../../fixtures/content.ts";
import { getUserByRole, TEST_USERS } from "../../fixtures/users.ts";

// Assertions
import {
  assertNotFound,
  assertSuccess,
  assertUnauthorized,
} from "../../utils/assertions.ts";

// Schema validation
import {
  assertSchemaValid,
  DomainSchema,
} from "../../utils/schema-validator.ts";