# Ruby testing

Test your integration without API calls using the [Ruby SDK](/docs/sdk/ruby).

## Mock client

```ruby
require 'calmlens/testing'
mock = CalmLens::MockClient.new # accepts project_id:, now:, create_id:, queued_status:, analyzed_status: and client settings
asset = mock.assets.moderate(name: 'note', content: {plain_text: 'hello', type: 'text/plain'})
mock.assets.retrieve(asset_id: asset['id'])['status'] # => "approved"
mock.assets.list(filter: 'status==approved')['total'] # => 1
mock.cdn.serve(asset_identifier: asset['id']).body    # => "hello"

request = mock.requests.last
request.method # => "GET"
request.path   # => "/assets/project-1/<asset id>/content"
```

The mock runs the real client's validation, encoding, retries and error handling, then answers from an in-memory stand-in for the API and CDN:

- Uploads create assets you can retrieve, list, search, move, update and delete. Folders, multipart uploads, idempotency keys and lenses behave like the API's.
- Errors use the API's codes, HTTP statuses and body shape, so `mock.assets.retrieve(asset_id: 'missing')` raises `CalmLens::NotFoundError` with status `404`.
- List filters support `==` and `!=`, with `*` wildcards, joined by `;` (and) and `,` (or), and ordering is applied. Other filter operators raise an error instead of being ignored.
- `cdn.serve` returns the stored bytes with `content-type`, `content-length` and `x-request-id` headers.

The mock does not classify content. `moderate` gives each asset `analyzed_status:`, which can be a status, fields to merge (`{'status' => 'rejected', 'report' => {'flagged' => true}}`) or a callable that receives the asset; `create` gives `queued_status:`. Change records through `mock.state`, such as `mock.state.assets[id]['status'] = 'rejected'`, to arrange a test.

## Events

```ruby
cursor = mock.events.list['cursor']
mock.enqueue_event(type: 'asset.approved', asset_id: asset['id'], facts: [{key: 'reviewer-name', value: 'Sam'}])
mock.events.each(after: cursor).map { |event| event['type'] } # => ["asset.approved"]
```

## Scripted responses and failures

Queued responses and errors answer the next requests in order, before the stand-in:

```ruby
mock.enqueue(status: 429, body: {code: 'rate/limited', message: 'Slow down'}, headers: {'retry-after' => '0'})
mock.enqueue_raw(body: 'not json')
mock.enqueue_error(Net::ReadTimeout.new)   # raises CalmLens::TimeoutError (sdk/timeout)
mock.enqueue_error(Errno::ECONNRESET.new)  # raises CalmLens::ConnectionError (sdk/transport-error)
```

Retries apply as in the real client, so a queued `503` on a read is retried. Pass `max_retries: 0` to see the first failure. `mock.requests` returns copies of every request received, with `method`, `url`, `path` (relative to the API or CDN base URL), decoded `query`, `headers` and `body`.
