Integration Testing Guide¶
Comprehensive guide for the E2E Integration Testing Framework for the Microlearning Platform API.
Table of Contents¶
- Testing Philosophy
- Architecture Overview
- Project Structure
- Local Environment Setup
- Test Infrastructure
- Writing Tests
- Running Tests
- CI/CD Integration
- Troubleshooting
- Maintenance Guide
Testing Philosophy¶
Core Principles¶
-
Isolation: Tests run in a dedicated local environment with a clean database reset before each test run, ensuring no interference between test runs.
-
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.
-
Real Infrastructure: Tests run against actual Supabase Edge Functions via
supabase functions serve, matching the production deployment structure as closely as possible. -
Known Test Data: Tests use seed data from SQL migrations (
20241229000002_seed_agentic_ai_curriculum.sql) providing predictable, known IDs for assertions. -
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:
Starting the Local Environment¶
The start script:
- Checks if local environment is already running
- Starts Supabase with the local configuration
- Waits for services to be ready
- Verifies health of API, Database, and Auth services
Stopping the Local Environment¶
Resetting the Database¶
This command:
- Runs
supabase db resetwhich drops all data - Re-applies all migrations in order
- Seeds the curriculum data from
20241229000002_seed_agentic_ai_curriculum.sql - Verifies seed data is present
Seeding Test Users¶
Creates test users with different subscription tiers:
| Role | 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¶
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:
- Verifies local environment is running on port 54321
- Serves edge functions with hot-reload using
.env.local - 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¶
-
Create file in appropriate directory:
-
Follow the standard structure with imports from setup/fixtures/utils
-
Run to verify:
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
mainordevelop - Pull requests to
main
Workflow Steps¶
- Checkout - Get code
- Setup Deno - Install Deno runtime
- Setup Supabase CLI - Install Supabase CLI
- Cache Dependencies - Cache Deno modules
- Start IT Environment - Start isolated Supabase
- Verify Supabase - Health check
- Reset Database - Apply migrations and seed data
- Seed Test Users - Create authenticated users
- Start Edge Functions - Serve functions in background
- Run Tests - Execute test suite with JUnit output
- Upload Results - Store test-results.xml artifact
- Generate Coverage - Create lcov report
- Upload to Codecov - Send coverage report
- Publish Results - Comment on PR with results
- 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:
Authentication Failures¶
Reset and reseed users:
Stale Test Data¶
Reset database:
Type Errors in Tests¶
Check imports and run type check:
Debug Mode¶
Run tests with verbose output:
Check function logs:
Maintenance Guide¶
Adding New Endpoints¶
-
Create test file:
-
Add test directory to deno.json (if new function):
Updating Seed Data¶
When seed data changes:
- Update
tests/e2e/fixtures/content.tswith new IDs - Update expected counts in tests
- Run full test suite to verify
Adding New Test Users¶
- Update
tests/scripts/seed-test-users.ts - Add to
tests/e2e/fixtures/users.ts - Add role to
UserRoleenum if new
Updating Schemas¶
When API schemas change:
- Update schemas in
tests/e2e/utils/schema-validator.ts - 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 | 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";