Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions docs/mock-api-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Mock API Module Boundaries and Consumer Contract

This document describes the architectural boundaries of the in-memory mock API, distinguishing between fixture data, behavior (simulation logic), and the application-facing API contract.

---

## Module Structure

### `lib/api/index.ts` — Public Application Entry Point

**Responsibility:** Sole import path for all API consumers.

**Exports:**
- `getApi(address?, token?, communityId?)` — Factory function returning `AccessApi` (mock or live based on `NEXT_PUBLIC_MOCK_MODE`)
- Types from `types.ts` (shared by mock and live clients)
- Type/utility exports from `mappers.ts`
- Error classes from `errors.ts`
- Mock-only developer controls (from `mock-boundary.ts`):
- `resetMockData()`
- `applyMockScenario()`
- `replayMockEvent()`
- `setMockRoleMutationFailure()`
- `setMockMetaVersion()`

**Consumer Contract:**
All application code (components, pages, tests) must import from `@/lib/api`. Never import from internal modules like `./mock.ts` or `./live.ts` in application code. The public surface provides the only stable boundary; internal module organization can change without breaking consumer contracts.

---

### `lib/api/mock-boundary.ts` — Stable Boundary for Mock-Only Functions

**Responsibility:** Wraps and re-exports mock-only developer controls, keeping the public surface deliberately small.

**What it exports:**
- `createMockAccessApi(address?, communityId?)` — Factory (not exported to `index.ts`, for internal use only)
- Re-exports of developer controls from `mock.ts`:
- `resetMockData()`
- `applyMockScenario()`
- `replayMockEvent()`
- `setMockMetaVersion()`
- `setMockRoleMutationFailure()`

**Design principle:** Application code receives only what it needs — no `MockAccessApi` class constructor, no internal storage APIs, no scenario enum. This boundary prevents coupling to implementation details while exposing enough for meaningful developer testing.

---

### `lib/api/mock.ts` — Implementation: Fixtures + Behavior

**Responsibility:** Core mock API implementation split into two concerns:

#### Fixtures (Static Data)
These are default/seeded data sets that populate the mock store:
- `DEFAULT_COMMUNITY` — Demo community configuration
- `DEFAULT_RESOURCES` — Gated resource catalog (alpha, pro-reports, mem-updates)
- `DEFAULT_POLICIES` — Access policies including composable rule examples
- `DEFAULT_WEBHOOK_EVENTS` — Example webhook event log entries
- `DEFAULT_MEMBER_STORE` — 50,000 synthetic seeded members with realistic names, tiers, roles

Fixtures initialize the mock state and are restored by `resetMockData()`. They are never directly imported by application code; they live in `mock.ts` to keep implementation details private.

#### Behavior (Simulation Logic)
These functions implement mock API behaviors:
- **SIWE simulation:** `getNonce()`, `siweVerify()`, `siweLogout()` (no real cryptography)
- **Session state:** Mock cookie reading/writing for `cookie` auth mode
- **Scenario presets:** Apply scripted test scenarios (active member, expired member, denied access, etc.)
- **Event replay:** Replay webhook events (e.g., for testing event-driven UI updates)
- **Failure injection:** `setMockRoleMutationFailure()`, `setMockResourceFetchFailure()`, `setMockResourceFetchDelay()` for failure mode testing
- **Version override:** `setMockMetaVersion()` for API compatibility testing

**MockAccessApi class:** Implements the `AccessApi` interface, providing:
- Read methods: `getSession()`, `getCommunity()`, `getMembers()`, `getMembership()`, etc.
- Write methods: `assignRole()`, `updatePolicy()`, `updateProfile()`, etc.
- SIWE methods: `getNonce()`, `siweVerify()`, `siweLogout()`

**Internal helpers:**
- `ensureAddress()` — Ensures a member record exists
- `randomHex()` — Generates mock nonces
- `throwMockUnauthorized()` — Simulates auth failures
- Community state management via `getCommunityState()`, `communityStates`

**Storage:** Delegates to `mock-storage.ts` for persistence via IndexedDB (with localStorage fallback).

---

### `lib/api/mock-storage.ts` — Mock State Persistence

**Responsibility:** Manage saving/loading/clearing mock state from IndexedDB (with localStorage fallback).

**Exports:**
- `loadPersistedState()` — Load saved mock state from storage
- `persistState(state)` — Save mock state asynchronously
- `clearPersistedState()` — Clear all stored mock data
- `LS_KEY` — Local storage key constant (for tests that need to validate storage)

**Note:** This module is internal; application code never directly calls these functions. Persistence is managed automatically by `MockAccessApi` during mutations and triggered by `resetMockData()`.

---

### `lib/api/types.ts` — Shared Type Definitions

**Responsibility:** Define the `AccessApi` interface and data models.

**Status:** Auto-generated from OpenAPI schema (`test/fixtures/openapi.json`) via `npm run sync-types`.

**Key exports:**
- `AccessApi` interface — Defines the contract all API clients (mock and live) must implement
- Data types: `Community`, `Resource`, `AccessPolicy`, `MemberRow`, `Membership`, `Role`, `MembershipTier`, `WebhookEventLog`, etc.
- Zod schemas for validation (e.g., `MembershipSchema`, `ResourceSchema`)

**Design principle:** Types are shared by mock and live implementations. If a type changes, both clients must adapt. This is enforced by the interface contract.

---

### `lib/api/live.ts` — Live Backend Integration

**Responsibility:** HTTP-based API client for live `guildpass-core` backend.

**Exports:**
- `LiveAccessApi` class — Implements `AccessApi` interface, making HTTP requests to the backend

**Relationship to mock:** Neither `live.ts` nor `mock.ts` import each other. Both implement the same `AccessApi` interface, allowing seamless switching via `getApi()` based on `NEXT_PUBLIC_MOCK_MODE`.

---

## Data Flow: How Components Use the API

```
Component (e.g., components/nav.tsx)
↓ imports
@/lib/api.index.ts
↓ calls getApi()
├─ if NEXT_PUBLIC_MOCK_MODE=true → createMockAccessApi() from mock-boundary.ts
│ ↓ instantiates
│ MockAccessApi from mock.ts
│ └─ reads/writes fixture data from mock-storage.ts
└─ else → LiveAccessApi from live.ts
↓ makes HTTP requests to guildpass-core
```

**Key point:** Components never directly reference `MockAccessApi`, `mock-storage.ts`, or fixture constants. This isolation means:
- Fixture data can be reorganized without affecting consumers
- Mock implementation details (storage strategy, scenario logic) can evolve without breaking imports
- Adding new dev-only controls (e.g., `setMockStatusCode()`) only requires changes to `mock.ts` and `mock-boundary.ts`, not consumer code

---

## Developer Controls (Mock-Only)

These functions are exported to `lib/api/index.ts` for use in developer UI and tests:

### `resetMockData()`
Clears all member, resource, policy, and event data and restores defaults.

**Used by:**
- `/developer` page (manual reset button)
- E2E tests (test fixture setup)

### `applyMockScenario(scenario, address?)`
Applies a scripted test scenario (e.g., "active member", "expired member", "denied resource").

**Scenarios:**
- `active-member` — Standard tier user with no special constraints
- `expired-member` — User with expired membership
- `denied-resource` — Free tier user with denied access to premium content
- `admin-session-expired` — User to test SIWE session expiry recovery
- `no-roles` — Member with no roles assigned
- `multiple-communities` — Member active across several communities

**Used by:**
- `/developer` page (scenario selector)
- Tests needing specific membership states

### `replayMockEvent(eventId)`
Replays a webhook event, triggering any downstream event-driven UI updates.

**Used by:**
- Admin event log (replay button)
- Tests of event-driven behavior

### `setMockRoleMutationFailure(shouldFail)`
Toggle whether role assignment mutations fail.

**Used by:**
- Error handling tests
- UI testing for failure states

### `setMockMetaVersion(version)`
Override the mock API's reported version for compatibility testing.

**Used by:**
- Version compatibility tests

---

## Fixture Maintenance Rules

When modifying fixtures in `mock.ts`:

1. **Changes are internal only** — Never export fixture constants. They are consumed by `MockAccessApi` methods only.
2. **Preserve defaults** — Default fixtures represent the "reset" state. `resetMockData()` restores them; any changes should be intentional.
3. **Seed stability** — The 50,000 synthetic members are generated deterministically; changing the seed names or generation logic affects all downstream member lookups and pagination tests.
4. **Scenario implications** — Fixture additions affect all scenarios. If adding a new resource, consider whether scenarios should reference it.
5. **Type alignment** — Ensure fixture data conforms to types in `types.ts`. Zod schemas help catch drift during testing.

---

## Behavior Implementation Rules

When adding new behavior to the mock (e.g., new test failure mode):

1. **Implement in `MockAccessApi` methods** — Behavior lives where it's called, not in separate helper files.
2. **Expose toggles via `mock-boundary.ts`** — If developers need to enable/disable the behavior, export a setter (e.g., `setMockXyzFail()`).
3. **Document in this file** — Add the new control to the "Developer Controls" section.
4. **Test both modes** — E2E tests should verify the behavior works in both mock and live modes (or skip live if the failure cannot be easily reproduced).

---

## Example: Tracing a Consumer's API Call

**Scenario:** Admin assigns a role in `/admin/members`.

1. Component calls `api.assignRole(memberAddress, role)` where `api = getApi(address, token)`
2. `getApi()` returns `MockAccessApi` (or `LiveAccessApi` in live mode)
3. `MockAccessApi.assignRole()`:
- Checks if `mockRoleMutationFailure` is set; if so, throws `AuthError`
- Otherwise, finds the member in `communityState.memberStore` and adds the role
- Persists the updated state via `persistState()` (to IndexedDB/localStorage)
- Returns the updated `MemberRow`
4. Component displays success/error based on result
5. If developer calls `setMockRoleMutationFailure(true)`, all subsequent role assignments fail until toggled back

The entire flow is transparent to the component; it only sees the `AccessApi` interface, never the mock implementation.

---

## Migration and Refactoring Guidance

### Safe Changes
These changes can happen without breaking consumers:
- Reorganizing fixture data within `mock.ts` (e.g., moving defaults into a separate file)
- Adding new developer controls to `mock-boundary.ts` and re-exporting them from `index.ts`
- Changing mock storage backend (e.g., from IndexedDB to SQLite) as long as `persistState()`/`loadPersistedState()` signature stays the same
- Optimizing fixture generation (e.g., lazy-loading the 50k members)

### Risky Changes
These require consumer updates:
- Adding/removing methods from `AccessApi` interface (affects both mock and live implementations)
- Changing data model shapes (affects all consumers expecting the old schema)
- Removing an exported developer control function (breaks tests using it)

---

## Related Documentation

- [Mock scenario presets](./mock-scenarios.md) — Detailed test scenarios and their membership states
- [Architecture](./architecture.md) — Full request/response flow, SIWE sequence, integration gateway
- [README: Feature Flags](../README.md#feature-flags) — Developer controls in the `/developer` UI