npm package: @pooflabs/test
Description: Test SDK for Poof API - Mocking and test automation
Author: Poof.new
Latest version: 0.0.6

# @pooflabs/test

Test SDK for TaroBase - Drop-in replacement for `@pooflabs/web` with mocking capabilities.

## Features

- **Zero Code Changes**: Works as a drop-in replacement for production SDK
- **Mock Authentication**: No real wallet needed - authenticate with any address
- **Test Overrides**: Mock user addresses, timestamps, and function returns
- **Parent-Child Communication**: Control tests from parent iframe via postMessage
- **Origin Validation**: Secure postMessage with origin allowlist (poof.new, v2.poof.new, localhost)
- **Full Observability**: Reports all API calls back to parent for monitoring
- **SessionStorage Based**: Configuration persists across page reloads during test

## Installation

```bash
npm install @pooflabs/test
# or
yarn add @pooflabs/test
```

## Quick Start

### 1. Build-Time Substitution

Replace imports during build:

```bash
# Use sed or your build tool to replace imports
sed -i "s/@pooflabs\/web/@pooflabs\/test/g" src/**/*.ts
```

### 2. Basic Usage (Standalone)

```typescript
import { init, get, set } from '@pooflabs/test';

// Set test user in sessionStorage
sessionStorage.setItem('test-user-address', 'FvgqHMfL3YxzcJp7VfqAbJC9pFLdvvZCgTVPfW8tCvp8');

// Initialize (works exactly like production SDK)
await init({
  appId: 'your-app-id',
  apiKey: 'your-api-key'
});

// Use normally - test headers automatically injected
const user = await get('/users/123');
await set('/users/123', { name: 'Test User' });
```

### 3. With Parent Iframe (Recommended)

**Security Note:** By default, only `poof.new`, `v2.poof.new`, and `localhost` origins can control the test iframe. See [PARENT_API.md](./PARENT_API.md#origin-security) for customization.

**Parent Window (Test Harness):**

```typescript
const testFrame = document.getElementById('test-iframe') as HTMLIFrameElement;

// Set test user
testFrame.contentWindow?.postMessage({
  type: 'setUser',
  payload: {
    address: 'FvgqHMfL3YxzcJp7VfqAbJC9pFLdvvZCgTVPfW8tCvp8',
    id: 'test-user-123'
  }
}, '*');

// Set time override
testFrame.contentWindow?.postMessage({
  type: 'setOverrides',
  payload: {
    timeOverride: '2025-01-01T00:00:00Z'
  }
}, '*');

// Set function overrides
testFrame.contentWindow?.postMessage({
  type: 'setOverrides',
  payload: {
    functionOverrides: {
      'plugin.checkBalance': { balance: 1000 },
      'plugin.getUserRole': 'admin'
    }
  }
}, '*');

// Get current state from child (including sessionStorage)
testFrame.contentWindow?.postMessage({ type: 'getState' }, '*');

// Disconnect user
testFrame.contentWindow?.postMessage({ type: 'disconnect' }, '*');

// Listen to events from test app
window.addEventListener('message', (event) => {
  console.log('Test app event:', event.data);

  switch (event.data.type) {
    case 'ready':
      console.log('App initialized');
      break;
    case 'apiCall':
      console.log('API call:', event.data.payload);
      break;
    case 'authStateChanged':
      console.log('Auth changed:', event.data.payload);
      break;
    case 'stateSnapshot':
      console.log('Current state:', event.data.payload);
      // payload includes: user, overrides, and all sessionStorage
      break;
  }
});
```

**Try the example:** Open `example-parent-harness.html` in your browser to see a working test harness that controls a child iframe.

**Child Window (Your App - No Code Changes):**

```typescript
// Your app code remains unchanged!
import { init, get, set } from '@pooflabs/web'; // Actually uses @pooflabs/test after build substitution

await init({ appId: 'your-app-id', apiKey: 'your-api-key' });

// Everything works normally, but with test overrides applied
const user = await get('/users/123');
```

## Test Headers

The following headers are automatically injected based on sessionStorage:

| Header | SessionStorage Key | Description |
|--------|-------------------|-------------|
| `x-test-user-address` | `test-user-address` | Override authenticated user address |
| `x-test-time-override` | `test-time-override` | Override server time (ISO 8601 string) |
| `x-func-{name}-override` | `test-function-overrides` | Override function return values |

### Function Override Format

Function names are converted: `plugin.functionName` → `x-func-plugin_functionname-override`

Example:
```typescript
// In test harness:
{
  functionOverrides: {
    'myPlugin.checkBalance': { balance: 1000 }
  }
}

// Results in header:
// x-func-myplugin_checkbalance-override: {"balance":1000}
```

## PostMessage API

### Parent → Child (Test Control)

```typescript
// Set user
{ type: 'setUser', payload: { address: string, id?: string } }

// Set overrides
{ type: 'setOverrides', payload: {
  timeOverride?: string,
  functionOverrides?: { [name: string]: any }
}}

// Disconnect user
{ type: 'disconnect' }

// Clear all overrides
{ type: 'clearOverrides' }

// Get current state (child will respond with stateSnapshot)
{ type: 'getState' }
```

### Child → Parent (Observability)

```typescript
// App ready
{ type: 'ready', payload: {} }

// API call made
{ type: 'apiCall', payload: {
  method: string,
  path: string,
  headers: Record<string, string>,
  timestamp: string
}}

// Auth state changed
{ type: 'authStateChanged', payload: {
  authenticated: boolean,
  user?: { address: string, id?: string }
}}

// State snapshot (sent when state changes or when parent requests it)
{ type: 'stateSnapshot', payload: {
  user: { address: string, id?: string } | null,
  overrides: {
    timeOverride?: string,
    functionOverrides?: { [name: string]: any }
  },
  sessionStorage: Record<string, string> // All 'test-*' keys
}}
```

## Advanced Usage

### Direct TestCoordinator Access

```typescript
import { testCoordinator } from '@pooflabs/test';

// Manually set user
testCoordinator.setUser({ address: '0x...', id: 'user-123' });

// Manually set overrides
testCoordinator.setOverrides({
  timeOverride: '2025-01-01T00:00:00Z',
  functionOverrides: { 'plugin.func': 'mocked-value' }
});

// Get current test headers
const headers = testCoordinator.getTestHeaders();

// Disconnect
testCoordinator.disconnect();
```

### Custom Auth Provider

```typescript
import { MockAuthProvider } from '@pooflabs/test';

const provider = new MockAuthProvider();
await provider.login(); // Reads from sessionStorage
```

## Architecture

```
┌─────────────────────────────────────────┐
│     Test Harness (Parent Window)        │
│  - Controls test execution               │
│  - Sends commands via postMessage        │
│  - Observes app behavior                 │
└─────────────┬───────────────────────────┘
              │ postMessage
              ↓
┌─────────────────────────────────────────┐
│     App (iframe with @pooflabs/test)   │
│  ┌──────────────────────────────────┐  │
│  │  TestCoordinator (auto-init)     │  │
│  │  - Listens to parent              │  │
│  │  - Manages sessionStorage         │  │
│  │  - Injects headers automatically  │  │
│  └──────────────────────────────────┘  │
│  ┌──────────────────────────────────┐  │
│  │  App Code (unchanged)            │  │
│  │  Uses @pooflabs/test via alias   │  │
│  └──────────────────────────────────┘  │
└─────────────────────────────────────────┘
```

## Differences from Production SDK

1. **Authentication**: Uses `MockAuthProvider` instead of real wallets
2. **Transactions**: Returns mock signatures (backend still processes with test headers)
3. **Auto-Injection**: Test headers automatically added to all API calls
4. **Observability**: Reports all operations to parent window
5. **No Wallet Bundle**: Smaller bundle size (no Privy/Phantom/etc.)

## SessionStorage Keys

- `test-user-address`: Current test user address
- `test-user-id`: Current test user ID (optional)
- `test-time-override`: Override server time
- `test-function-overrides`: JSON string of function overrides

## License

MIT