Skip to main content

README.md

# http_server_mock

A WireMock-style HTTP mock server library for Gleam, running on both Erlang and JavaScript targets. Start a real HTTP server in your tests, register stubs that describe how it should respond, make real HTTP calls against it, then inspect recorded requests to verify what happened.

## Installation

Add the core package and a runtime package for your target:

**Erlang:**
```sh
gleam add http_server_mock http_server_mock_erlang
```

**JavaScript:**
```sh
gleam add http_server_mock http_server_mock_js
```

## Quick start

```gleam
import gleam/http
import http_server_mock
import http_server_mock_erlang  // or http_server_mock_js
import http_server_mock/matcher
import http_server_mock/response
import http_server_mock/stub_builder
import http_server_mock/verify

pub fn my_test() {
  let server =
    http_server_mock.new(http_server_mock_erlang.server())
    |> http_server_mock.with_stub(
      stub_builder.new()
      |> stub_builder.matching(
        matcher.new()
        |> matcher.method(http.Get)
        |> matcher.path("/greet"),
      )
      |> stub_builder.responding_with(
        response.new()
        |> response.status(200)
        |> response.body("hello"),
      )
      |> stub_builder.build(),
    )
    |> http_server_mock.start()

  // Make real HTTP calls against the server
  let url = http_server_mock.base_url(server) <> "/greet"
  // ... your HTTP client call here ...

  verify.called(server, matcher.new() |> matcher.path("/greet"))

  http_server_mock.stop(server)
}
```

## Server lifecycle

```gleam
// Create — picks a random free port by default
let server = http_server_mock.new(adapter)

// Optionally pin a port
let server = http_server_mock.new(adapter) |> http_server_mock.with_port(8080)

// Start
let server = http_server_mock.start(server)

// Get the base URL for your HTTP client
let url = http_server_mock.base_url(server)  // "http://localhost:54321"

// Stop
let server = http_server_mock.stop(server)
```

Phantom types (`NotStarted`, `Started`, `Stopped`) enforce correct usage at compile time — passing a stopped server to `add_stub` or `base_url` is a type error.

## Stubs

### Registering stubs

```gleam
// Panics on failure — use for chaining during setup
http_server_mock.with_stub(server, stub)

// Returns Result — use when you want to handle failure
http_server_mock.add_stub(server, stub)

// Remove by ID
http_server_mock.remove_stub(server, stub_id)

// Remove all
http_server_mock.reset_stubs(server)
```

### Building a stub

```gleam
stub_builder.new()
|> stub_builder.matching(request_matcher)
|> stub_builder.responding_with(response_definition)
|> stub_builder.with_id("my-stub")       // optional custom ID
|> stub_builder.with_priority(1)          // lower wins; default is 5
|> stub_builder.build()
```

## Matchers

Start with `matcher.new()` (matches everything) and add constraints:

```gleam
matcher.new()
|> matcher.method(http.Post)
|> matcher.path("/users")
|> matcher.path_contains("/users")                    // substring
|> matcher.path_matching(types.Prefix("/api/"))       // StringMatcher
|> matcher.query_param("page", types.Exactly("2"))
|> matcher.header("Authorization", types.Prefix("Bearer "))
|> matcher.body_json("{\"key\":\"value\"}")           // exact JSON body
```

## Responses

```gleam
response.new()                            // 200, no headers, no body
|> response.status(201)
|> response.body("plain text")
|> response.json_body("{\"id\":1}")       // sets Content-Type: application/json
|> response.header("X-Custom", "value")
|> response.delay(200)                    // milliseconds

response.ok()                             // shorthand for 200 with no body
```

## Verification

Verify functions assert and return the matched recorded requests, or panic with a descriptive message.

```gleam
verify.called(server, matcher)                  // at least once
verify.called_times(server, matcher, 3)         // exactly 3 times
verify.called_at_least(server, matcher, 2)      // at least 2 times
verify.never_called(server, matcher)            // zero times
```

### Inspecting recorded requests

```gleam
let assert Ok(requests) = http_server_mock.recorded_requests(server)
let assert Ok(unmatched) = http_server_mock.unmatched_requests(server)

http_server_mock.reset_requests(server)    // clear history
http_server_mock.reset(server)             // clear stubs + history
```

## Scenarios (stateful stubs)

Scenarios let you model sequences of responses from the same endpoint.

```gleam
let initial_stub =
  stub_builder.new()
  |> stub_builder.matching(matcher.new() |> matcher.path("/state"))
  |> stub_builder.responding_with(response.new() |> response.body("first"))
  |> stub_builder.in_scenario("my-scenario")
  |> stub_builder.when_state_is(types.ScenarioStarted)
  |> stub_builder.then_transition_to("second-call")
  |> stub_builder.build()

let second_stub =
  stub_builder.new()
  |> stub_builder.matching(matcher.new() |> matcher.path("/state"))
  |> stub_builder.responding_with(response.new() |> response.body("second"))
  |> stub_builder.in_scenario("my-scenario")
  |> stub_builder.when_state_is("second-call")
  |> stub_builder.build()
```

## Runtimes

| Package | Target | Underlying server |
|---|---|---|
| `http_server_mock_erlang` | Erlang/OTP | mist + OTP actor |
| `http_server_mock_js` | JavaScript | Node.js `http` module in a Worker thread |

Pass the adapter from the runtime package to `http_server_mock.new/1`:

```gleam
// Erlang
http_server_mock.new(http_server_mock_erlang.server())

// JavaScript
http_server_mock.new(http_server_mock_js.server())
```

## License

MIT