npm package: @insureco/test-sdk
Description: Mock clients and test utilities for Tawa platform services
Latest version: 0.1.0

# @insureco/test-sdk

Mock clients for every Tawa platform service. Drop these into your unit tests so they never hit real infrastructure. Set `TAWA_ENV=sandbox` and the same test file runs against real sandbox services for integration testing.

## Install

```bash
npm install --save-dev @insureco/test-sdk
```

## Quick start

```typescript
import { mockRelay, mockWallet, mockSeptor, mockKoko, mockBioAuth } from '@insureco/test-sdk'
import { describe, it, beforeEach } from 'vitest'

describe('OrderService', () => {
  const relay = mockRelay()
  const wallet = mockWallet({ balance: 50000 })
  const septor = mockSeptor()

  beforeEach(() => {
    relay.reset()
    wallet.reset()
    septor.reset()
  })

  it('charges gas and sends confirmation email', async () => {
    const service = new OrderService({ relay, wallet, septor })

    await service.placeOrder({ userId: 'u-1', amount: 99 })

    wallet.expectDebit({ amount: 5 })
    wallet.expectBalance(49995)
    relay.expectEmailSent({ to: 'u-1@example.com', subject: /order confirmed/i })
    septor.expectEventEmitted('order.placed', { entityId: 'u-1' })
  })
})
```

---

## Mock factories

### `mockRelay(options?)`

Mock for `@insureco/relay` — email and SMS delivery.

```typescript
const relay = mockRelay()

// Send email
await relay.sendEmail({
  to: { email: 'user@example.com', name: 'User' },
  template: 'welcome',
  data: { name: 'User' },
})

// Send SMS
await relay.sendSMS({
  to: { phone: '+1234567890' },
  content: { text: 'Your code is 1234' },
})
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectEmailSent({ to?, subject? })` | At least one email matches. `subject` accepts string or RegExp. |
| `expectSMSSent({ to?, text? })` | At least one SMS matches. `text` accepts string or RegExp. |
| `expectNoEmailSent()` | No emails were sent. |
| `expectNoSMSSent()` | No SMS were sent. |

**Accessors:** `relay.sentEmails`, `relay.sentSMS`

**Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `templates` | `['welcome', 'invitation', 'password-reset', 'magic-link']` | Templates returned by `listTemplates()` |

---

### `mockWallet(options?)`

Mock for `@insureco/wallet` — token balance, debits, credits, deploy gate.

```typescript
const wallet = mockWallet({ balance: 50000 })

await wallet.debit('wallet-my-org', { amount: 100, reason: 'API call' })
wallet.expectBalance(49900)

await wallet.credit('wallet-my-org', { amount: 500, tokenType: 'purchased', reason: 'Top-up' })
wallet.expectBalance(50400)

// Deploy gate check
const reserve = await wallet.checkReserve('wallet-my-org', { podTier: 'nano' })
// reserve.sufficient === true (need 10800 tokens for 3 months of nano)
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectDebit({ amount?, reason? })` | At least one debit matches. |
| `expectCredit({ amount?, tokenType? })` | At least one credit matches. |
| `expectBalance(n)` | Current balance equals `n`. |
| `expectNoDebit()` | No debits occurred. |

**Accessors:** `wallet.currentBalance`, `wallet.ledger`

**Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `balance` | `50000` | Starting token balance |
| `tokenType` | `'intro'` | Default token type |
| `walletId` | `'wallet-mock-org'` | Default wallet ID |

---

### `mockSeptor(options?)`

Mock for `@insureco/septor` — immutable audit trail with hash chains.

```typescript
const septor = mockSeptor({ namespace: 'my-service' })

await septor.emit('payment.processed', {
  entityId: 'pay-123',
  data: { amount: 100, currency: 'USD' },
  metadata: { who: 'system', why: 'Auto-charge' },
})

septor.expectEventEmitted('payment.processed', { entityId: 'pay-123' })
septor.expectChainValid('pay-123')
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectEventEmitted(type, { entityId?, data? })` | Event of given type was emitted matching filters. |
| `expectNoEventsEmitted()` | Nothing was emitted. |
| `expectChainValid(entityId)` | Hash chain for entity is intact. |

**Accessors:** `septor.events`

The mock maintains real hash chains — each event's `previousHash` links to the prior event's `eventHash`, and `chainIndex` increments per entity.

---

### `mockKoko(options?)`

Mock for Koko service registry and namespace config.

```typescript
const koko = mockKoko()

// Pre-configure a namespace
koko.setNamespace('my-org-sandbox', { transactionMultiplier: 0.1, rateLimit: 1000 })

// Register a service
await koko.register({
  name: 'my-api',
  namespace: 'my-org-sandbox',
  url: 'http://my-api:3000',
  routes: [{ path: '/api/users', methods: ['GET'], auth: 'required' }],
})

// Discover
const svc = await koko.discover('my-api')

koko.expectServiceRegistered('my-api')
koko.expectServiceDiscovered('my-api')
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectServiceRegistered(name)` | Service was registered via `register()`. |
| `expectServiceDiscovered(name)` | `discover()` was called for this service. |

**Accessors:** `koko.registeredServices`

**Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `services` | `[]` | Pre-seeded services available for discovery |

---

### `mockBioAuth(options?)`

Mock for `@insureco/bio` `BioAuth` — OAuth flows, token exchange, PKCE, introspection.

```typescript
const auth = mockBioAuth({
  user: { bioId: 'bio-user-1', email: 'alice@acme.com', name: 'Alice', orgSlug: 'acme' },
})

// Authorization URL (PKCE)
const { url, state, codeVerifier } = auth.getAuthorizationUrl({
  redirectUri: 'https://my-app.tawa.insureco.io/api/auth/callback',
  scopes: ['openid', 'profile'],
})

// Exchange code for tokens
const tokens = await auth.exchangeCode('auth-code-123', codeVerifier, redirectUri)
auth.expectTokenExchanged({ code: 'auth-code-123' })

// Get user info
const user = await auth.getUserInfo(tokens.access_token)
// user.email === 'alice@acme.com'

// Client credentials (service-to-service)
await auth.getClientCredentialsToken(['service:read'])
auth.expectClientCredentialsUsed({ scopes: ['service:read'] })

// Revocation + introspection
await auth.revokeToken(tokens.access_token)
const result = await auth.introspect(tokens.access_token)
// result.active === false
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectTokenExchanged({ code? })` | `exchangeCode` was called, optionally matching the auth code. |
| `expectTokenRefreshed()` | `refreshToken` was called. |
| `expectTokenRevoked({ token? })` | A token was revoked. |
| `expectClientCredentialsUsed({ scopes? })` | Client credentials flow was used. |
| `expectIntrospected({ token? })` | `introspect` was called. |

**Accessors:** `auth.issuedTokens`, `auth.revokedTokens`

**Mutation:** `auth.setUser({ name: 'New Name' })` — changes what `getUserInfo` returns.

---

### `mockBioAdmin(options?)`

Mock for `@insureco/bio` `BioAdmin` — user management, departments, roles, OAuth clients.

```typescript
const admin = mockBioAdmin({
  users: [
    { bioId: 'bio-1', email: 'alice@acme.com', name: 'Alice', roles: ['admin'], ... },
  ],
})

// User management
const alice = await admin.getUser('bio-1')
await admin.updateUser('bio-1', { name: 'Alice Smith' })
admin.expectUserUpdated('bio-1', { name: 'Alice Smith' })

// Create resources
await admin.createDepartment({ name: 'Engineering' })
await admin.createRole({ name: 'editor', permissions: ['read', 'write'] })
await admin.createClient({ name: 'my-app', redirectUris: ['https://app/callback'] })

admin.expectDepartmentCreated({ name: 'Engineering' })
admin.expectRoleCreated({ name: 'editor' })
admin.expectClientCreated({ name: 'my-app' })
```

**Assertions:**

| Method | Description |
|--------|-------------|
| `expectUserUpdated(bioId, data?)` | User was updated, optionally matching fields. |
| `expectDepartmentCreated({ name? })` | A department was created. |
| `expectRoleCreated({ name? })` | A role was created. |
| `expectClientCreated({ name? })` | An OAuth client was created. |

**Accessors:** `admin.users`, `admin.departments`, `admin.roles`, `admin.oauthClients`

**Mutation:** `admin.addUser(user)` — seed additional users at runtime.

---

## Common assertions (all mocks)

Every mock inherits these from the shared tracker:

| Method | Description |
|--------|-------------|
| `expectCalled(method, match?)` | Assert method was called. `match` checks properties on the first arg. |
| `expectNotCalled(method)` | Assert method was never called. |
| `getCalls(method?)` | Returns `CallRecord[]` with `{ method, args, returnValue, timestamp }`. |
| `reset()` | Clear all internal state and call history back to initial config. |

---

## TAWA_ENV: unit tests vs integration tests

```typescript
import { createClients } from '@insureco/test-sdk'

const { relay, wallet, septor, koko } = createClients()
```

| `TAWA_ENV` | Behavior |
|------------|----------|
| _(unset)_ or `test` | Returns mock clients. No network calls. |
| `sandbox` | Returns real SDK clients pointed at `*.sandbox.tawa.insureco.io`. Requires `@insureco/relay`, `@insureco/wallet`, `@insureco/septor` installed. |
| `production` | Returns real SDK clients pointed at `*.tawa.insureco.io`. |

This lets the same test file work as a fast unit test (default) or a real integration test:

```bash
# Unit tests (mocks)
npm test

# Integration tests (real sandbox)
TAWA_ENV=sandbox npm test
```

---

## Development

```bash
npm install
npm test            # vitest — 159 tests
npm run build       # ESM + CJS dual output
```