README.md

# Dream Mock Server

A general-purpose HTTP mock server developed by Dream that provides both streaming and non-streaming endpoints for testing HTTP clients.

**Current module version:** `1.1.0`

## Overview

This module provides a comprehensive set of endpoints for testing HTTP clients:

- **Non-streaming endpoints** - Standard REST-like endpoints (GET, POST, PUT, DELETE)
- **Streaming endpoints** - Various streaming behaviors for testing chunked responses

All endpoints are deterministic and designed for testing, making them ideal for:

- **Testing** - Use as a test fixture for HTTP client integration tests
- **Demo** - Showcase Dream's HTTP capabilities with predictable behavior
- **Learning** - Study different HTTP patterns and error handling

## Running

### Standalone Mode

Start the server on port 3004:

```bash
make run
```

Then access endpoints at `http://localhost:3004/`

### Programmatic Mode

Use in tests or other applications:

```gleam
import dream_mock_server/server

pub fn my_test() {
  let assert Ok(handle) = server.start(3004)
  
  // Make HTTP requests to localhost:3004
  // ... test logic ...
  
  server.stop(handle)
}
```
<sub>๐Ÿงช [Tested source](test/snippets/programmatic_mode.gleam)</sub>

### Config mode

For proxy tests or any test that needs a **deterministic upstream** with specific status and body per path, start the server with a config list. The mock has no built-in provider logic (no OpenAI, Google, AWS, etc.); the caller supplies all path โ†’ (status, body) mappings. First matching route in the list wins.

```gleam
import dream_mock_server/config.{MockRoute, Prefix}
import dream_mock_server/server
import gleam/option.{None}

// One route: path /v1/chat/completions (prefix match), any method, 200 + JSON body
let config = [
  MockRoute(
    path: "/v1/chat/completions",
    path_match: Prefix,
    method: None,
    status: 200,
    body: "{\"choices\":[{\"message\":{\"content\":\"\"}}]}",
  ),
]
let assert Ok(handle) = server.start_with_config(3004, config)
// Point your proxy or client at http://127.0.0.1:3004 ...
server.stop(handle)
```
<sub>๐Ÿงช [Tested source](test/snippets/config_mode_prefix.gleam)</sub>

#### How matching works

- Matching is **first route wins** in list order.
- `Exact` means request path must equal `path`.
- `Prefix` means request path must start with `path`.
- `method: Some(Get)` (or Post/Put/Delete/Patch) restricts by method.
- `method: None` matches any method.
- No match returns `404` with body `"Not found"`.

#### Exact + method-specific example

```gleam
import dream/http/request.{Get, Post}
import dream_mock_server/config.{Exact, MockRoute}
import dream_mock_server/server
import gleam/option.{Some}

let config = [
  MockRoute("/resource", Exact, Some(Get), 200, "{\"ok\":true}"),
  MockRoute("/resource", Exact, Some(Post), 201, "{\"created\":true}"),
]
let assert Ok(handle) = server.start_with_config(3004, config)
// GET /resource  -> 200 {"ok":true}
// POST /resource -> 201 {"created":true}
server.stop(handle)
```
<sub>๐Ÿงช [Tested source](test/snippets/config_mode_method_specific.gleam)</sub>

#### Ordering gotcha (important)

If you mix broad prefixes and specific routes, put specific routes first:

```gleam
// Good: specific before broad prefix
[
  MockRoute("/api/special", Exact, None, 200, "special"),
  MockRoute("/api", Prefix, None, 200, "general"),
]
```
<sub>๐Ÿงช [Tested source](test/snippets/config_mode_ordering.gleam)</sub>

- **Path match:** `Exact` (path must equal) or `Prefix` (path must start with).
- **Method:** `Some(Get)` etc. to restrict; `None` for any method.
- **Order:** Put more specific routes before broader prefix routes so they match first.
- **No match:** Returns 404 with body `"Not found"`.

## Non-Streaming Endpoints

### `GET /get`

**Returns JSON with request info**

- Returns: `{"method": "GET", "url": "/get", "headers": []}`
- Status: 200 OK
- Content-Type: `application/json`

**Use for:** Testing GET requests, JSON parsing

### `POST /post`

**Echoes request body as JSON**

- Returns: `{"method": "POST", "url": "/post", "data": "<request body>"}`
- Status: 201 Created
- Content-Type: `application/json`

**Use for:** Testing POST requests, request body handling

### `PUT /put`

**Echoes request body as JSON**

- Returns: `{"method": "PUT", "url": "/put", "data": "<request body>"}`
- Status: 200 OK
- Content-Type: `application/json`

**Use for:** Testing PUT requests

### `DELETE /delete`

**Returns success response**

- Returns: `{"method": "DELETE", "url": "/delete"}`
- Status: 200 OK
- Content-Type: `application/json`

**Use for:** Testing DELETE requests

### `GET /json`

**Returns simple JSON object**

- Returns: `{"message": "Hello, World!", "success": true}`
- Status: 200 OK
- Content-Type: `application/json`

**Use for:** Testing JSON parsing, simple responses

### `GET /text`

**Returns plain text**

- Returns: `"Hello, World!"`
- Status: 200 OK
- Content-Type: `text/plain`

**Use for:** Testing text responses

### `GET /uuid`

**Returns UUID-like string**

- Returns: `{"uuid": "123e4567-e89b-12d3-a456-426614174000"}`
- Status: 200 OK
- Content-Type: `application/json`

**Use for:** Testing small JSON responses

### `GET /`

**Info page** - Lists all available endpoints with descriptions

**Use for:** Documentation, service discovery

## Streaming Endpoints

### `GET /stream/fast`

**10 chunks at 100ms intervals**

- Fast streaming for quick response handling
- Total duration: ~1 second
- Each chunk: `"Chunk N\n"`

**Use for:** Testing fast streaming, basic functionality

### `GET /stream/slow`

**5 chunks at 2s intervals**

- Slow streaming for timeout handling
- Total duration: ~10 seconds
- Each chunk: `"Chunk N\n"`

**Use for:** Testing timeout handling, long-running streams

### `GET /stream/burst`

**7 chunks with variable delays (100-500ms)**

- Deterministic burst pattern for robustness testing
- Delays: `[100, 500, 200, 400, 100, 300, 200]` ms
- Each chunk: `"Burst N\n"`

**Use for:** Testing variable timing patterns, buffering behavior

### `GET /stream/error`

**3 chunks then 500 status**

- Returns HTTP 500 with partial content
- Simulates mid-stream error
- Each chunk: `"Error test chunk N\n"`

**Use for:** Testing error handling mid-stream

### `GET /stream/huge`

**100 chunks with 10ms delays**

- Large response for memory/performance testing
- Total duration: ~1 second
- Each chunk: `"Huge chunk N\n"`

**Use for:** Memory efficiency, performance testing

### `GET /stream/json`

**5 JSON objects at 200ms intervals**

- Structured data streaming
- Each chunk: `{"event":"chunk","number":N,"message":"JSON event N"}\n`
- Content-Type: `application/json`

**Use for:** Testing structured data streaming, JSON handling

### `GET /stream/binary`

**256 binary chunks with byte patterns**

- Non-text streaming
- Each chunk: 4 bytes with repeating pattern
- Content-Type: `application/octet-stream`

**Use for:** Testing binary data streaming

## Integration Tests

Run Cucumber integration tests:

```bash
make test-integration
```

Tests verify:
- All endpoints return correct status codes
- Content format and structure
- Chunk counts and timing
- Error handling
- JSON validity

## Usage Modes

### 1. Standalone Server

Fixed port 3004, runs indefinitely:

```bash
make run
```

### 2. Programmatic Control

Start/stop programmatically for tests:

```gleam
import dream_mock_server/server

// Start server
let assert Ok(handle) = server.start(3004)

// Use server...

// Stop server
server.stop(handle)
```

### 3. Integration Testing

Used by Dream's HTTP client tests:

```gleam
// In modules/http_client/test/integration_test.gleam
import dream_mock_server/server

pub fn concurrent_streams_test() {
  let assert Ok(mock) = server.start(3004)
  
  // Test HTTP client against localhost:3004
  // ...
  
  server.stop(mock)
}
```

## Implementation Details

### Streaming Technique

Streaming endpoints use Dream's `response.stream_response()`:

```gleam
import dream/http/response.{stream_response}
import dream/http/status
import gleam/yielder
import gleam/erlang/process

pub fn stream_fast(...) -> Response {
  let stream =
    yielder.range(1, 10)
    |> yielder.map(fn(n) {
      process.sleep(100)
      let line = "Chunk " <> int.to_string(n) <> "\n"
      bit_array.from_string(line)
    })

  stream_response(status.ok, stream, "text/plain")
}
```

### Non-Streaming Technique

Non-streaming endpoints use standard `json_response()` and `text_response()`:

```gleam
import dream/http/response.{json_response}
import dream/http/status
import gleam/json

pub fn get(...) -> Response {
  let body =
    json.object([
      #("method", json.string("GET")),
      #("url", json.string(request.path)),
    ])
    |> json.to_string
  
  json_response(status.ok, body)
}
```

### Timing Control

Streaming endpoints use `process.sleep()` between chunks for deterministic timing:

- **Fast**: 100ms delays
- **Slow**: 2000ms delays
- **Burst**: Variable 100-500ms delays
- **Huge**: 10ms delays (optimized for throughput)

### Error Simulation

The `/stream/error` endpoint demonstrates mid-stream errors by returning a 500 status with partial content. This tests client error handling during streaming.

## Architecture

```
src/
โ”œโ”€โ”€ main.gleam                    # Standalone entry point (port 3004)
โ”œโ”€โ”€ dream_mock_server/
โ”‚   โ”œโ”€โ”€ server.gleam             # Programmatic start/stop functions
โ”‚   โ”œโ”€โ”€ router.gleam             # Route definitions
โ”‚   โ”œโ”€โ”€ controllers/
โ”‚   โ”‚   โ”œโ”€โ”€ api_controller.gleam      # Non-streaming endpoints
โ”‚   โ”‚   โ””โ”€โ”€ stream_controller.gleam   # Streaming endpoints
โ”‚   โ””โ”€โ”€ views/
โ”‚       โ””โ”€โ”€ index_view.gleam          # Info page HTML
```

## Dependencies

- **Dream** - Web framework
- **gleam_erlang** - Process control (sleep)
- **gleam_json** - JSON encoding
- **gleam_yielder** - Stream generation

## All Examples Are Tested

The core usage examples in this README are backed by runnable snippet tests
under `test/snippets/` and executed by `test/snippets_test.gleam`.

Run them with:

```bash
cd modules/mock_server
gleam test
```

## See Also

- [Dream Streaming Guide](../../docs/guides/streaming.md)
- [HTTP Client Module](../../modules/http_client/)
- [Streaming Capabilities Example](../streaming_capabilities/)