Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Protocol-neutral async management prefix `/_mock/async/{push,consumers,disconnect}`; legacy `/_mock/ws/*` kept as deprecated aliases
- Unified example injection: `POST /_mock/examples` gains `match`/`interval`/`delay` for AsyncAPI targets (runtime mirror of `x-mock-match`/`x-mock-interval`/`x-mock-delay`), with strict context-aware validation, plus `DELETE /_mock/examples/{exampleId}` to remove and cancel recurrence
- Single event resource `POST /_mock/events` with a `type` discriminator (V1: `fire`), replacing `/_mock/events/fire` (deprecated alias kept)
- Management WebSocket stream `/_mock/stream` with connect-time `events`/`channels` filters; pushes `event`/`push`/`consumer`/`schedule` envelopes
- Event-context matching: `{$event.name}` (identity), `{$event.data}` (whole payload) alongside `{$event.<field>}`; `{$connection.*}` per-connection recipient partition (id/channel/query/header) with broadcast fast path
- Timing extensions `x-mock-interval` (periodic emission) and `x-mock-delay` (delayed emission); `cron` is no longer an event
- Actually-fired built-in triggers `connect` (on consumer connection) and `receive` (on inbound traffic), gated by a cheap `hasSubscribers` check
- Consumers listable without a `channel` filter — flat union across all channels (raw ws + SignalR streams)
- `x-send-events` is deprecated: a loader mapping shim translates `{on, wait}` to the match/interval form with a verbose deprecation note; removal deferred one release

### Changed
- Recurring delivery moved off the schedule endpoint onto `interval` on `/_mock/examples`; `/_mock/ws/schedule{,/{pushId}}` now answer `410 Gone` pointing at the examples endpoint
- `AddExampleRequest` is now a `oneOf` two-branch schema (sync `path` vs async `channel`) rejecting mixed targeting
- Delivered/scheduled messages are templated at emission time so `{$event.*}`/`{$state.*}`/`{$env.*}` resolve against current state

### Fixed
- `x-mock-delay` now actually delays an async emission (it was parsed but never applied); a `connect` welcome honors it too
- The deprecated `/_mock/events/fire` alias again accepts the legacy type-less body shape (defaulting to `fire`) instead of requiring the new `type` discriminator
- A runtime async `match` without an `{$event.*}` reference is rejected with 400 instead of silently registering nothing
- `DELETE /_mock/examples/{exampleId}` now also removes sync (OpenAPI) dynamic examples, not only async-driven ones
- The `/_mock/stream` ping keepalive goroutine no longer leaks past the connection's lifetime
- Schema registration is atomic: a load/classification error from any example aborts the whole schema without leaking already-started interval jobs
- A periodically driven example is single-trigger — declaring `x-mock-interval` together with any `x-mock-match` is rejected at load (previously the match was silently dropped at delivery), and period examples honor `x-mock-skip`
- An `{$event.name}` identity whose value is itself a runtime expression is rejected at load instead of registering a subscription key that could never match (matches without an identity pin stay wildcard)
- Timing extensions require integer milliseconds: fractional `x-mock-interval`/`x-mock-delay` values are load errors rather than silently truncated
- A panicking interval delivery is recovered: the job is unregistered and logged instead of silently losing its cadence
- SignalR built-in `connect`/`receive` use a deterministic default channel address instead of map-iteration order for multi-channel hubs
- Periodic deliveries now emit `push` envelopes and built-in `connect` fires emit `event` envelopes to `/_mock/stream` subscribers; `schedule` `started`/`stopped` envelopes carry the same example identity, channel and interval so clients can correlate them
- SignalR upgrades now capture query/headers so `{$connection.query.*}`/`{$connection.header.*}` resolve for hub connections too
- `{$event.*}`/`{$connection.*}` condition values pre-resolve at delivery; reply-path condition values stay literal (sync matching unchanged)
- Legacy `x-send-events {on: cron}` without a positive `wait` is a load error instead of a silent no-op
- Docker image `/app/oasmock` is now marked executable — GitHub artifact downloads strip exec bits, breaking `ENTRYPOINT` in the published image
- CI-built binaries are now statically linked (`CGO_ENABLED=0`) — previously `linux/amd64` was dynamically linked against glibc, causing `exec /app/oasmock: no such file or directory` in the `distroless/static` image
- Release Docker image is now smoke-tested (starts and serves the control API) before it is pushed to Docker Hub, via a shared `smoke-test-image` action also used by the PR `docker-build` check
- `api/openapi.yaml` was an invalid OpenAPI document (array schemas missing `items`), causing the server to exit at startup and the Docker smoke test to fail with connection refused; now fixed and covered by a loader test
- Docker smoke test now waits for server readiness with a retry loop and dumps container logs on failure for diagnosis

## [0.1.0] - Initial Release

### Added
- Initial release of OASMock - OpenAPI mock server
- Support for OpenAPI 3.0 schemas with custom extensions
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,27 @@ Route calls by body field instead of URL path. See [json-rpc.md](./docs/json-rpc

## Runtime Expressions

Runtime expressions are enclosed in `{$...}` and resolved at request time. Data sources: `{$request.path.param}`, `{$request.query.param}`, `{$request.header.name}`, `{$request.body.field}`, `{$request.cookie.name}`, `{$state.key}`, `{$env.VARIABLE}`.
Runtime expressions are enclosed in `{$...}` and resolved at request time. Data sources: `{$request.path.param}`, `{$request.query.param}`, `{$request.header.name}`, `{$request.body.field}`, `{$request.cookie.name}`, `{$state.key}`, `{$env.VARIABLE}`, and for async-driven examples `{$event.name}`/`{$event.data}`/`{$event.<field>}` plus per-connection `{$connection.id}`/`{$connection.channel}`/`{$connection.query.<key>}`/`{$connection.header.<key>}`.

Modifiers: `\|default:value` (fallback), `\|getByPath:path` (traverse nested objects), `\|toJWT` (stub).

Expressions can appear in extension keys, values, and response bodies. Full reference: [extensions.md](./docs/extensions.md#runtime-expressions).

## Management API

The server exposes a control HTTP API under the `/_mock` prefix. Full schema: [api/openapi.yaml](./api/openapi.yaml).
The server exposes a control HTTP API under the `/_mock` prefix. Full schema: [api/openapi.yaml](./api/openapi.yaml). The asynchronous control surface (the management WebSocket stream `/_mock/stream` and its envelopes) is described in [api/asyncapi.yaml](./api/asyncapi.yaml). Both specs are kept in sync with the implementation by contract tests in `internal/server/control_api_spec_sync_test.go`.

- `GET /_mock/requests` — request history (filterable by path, method, time range, pagination)
- `POST /_mock/examples` — add a dynamic example to an existing route
- sync (OpenAPI) targets use `path`; AsyncAPI targets use `channel` with optional `match`/`interval`/`delay` mirroring `x-mock-match`/`x-mock-interval`/`x-mock-delay` for live event-driven or recurring delivery
- `DELETE /_mock/examples/{exampleId}` — remove a dynamic example and cancel any recurring interval delivery
- `POST /_mock/events` — fire a named event ad-hoc with a `type` discriminator (`fire` for V1)
- `POST /_mock/async/push` — push a message to channel consumers (immediate/delayed, targeted/broadcast)
- `GET /_mock/async/consumers` — list connected consumers (`channel` optional, all channels when omitted)
- `POST /_mock/async/disconnect` — force-disconnect a consumer
- `GET /_mock/stream` — management WebSocket stream of runtime notifications (event/push/consumer/schedule envelopes, filtered at connect time)

The legacy `/_mock/ws/*` aliases and `/_mock/events/fire` are deprecated but still work; the removed `/_mock/ws/schedule*` answers `410 Gone` pointing at the examples endpoint.

## Command‑Line Interface

Expand Down
185 changes: 185 additions & 0 deletions api/asyncapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
asyncapi: 3.0.0
info:
title: OASMock control AsyncAPI
description: |
Asynchronous surface of the OASMock control plane. The management WebSocket
stream (GET /_mock/stream) is the general cross-cutting control channel for
mock-runner test harnesses: it pushes runtime notifications (fired events,
message pushes, consumer lifecycle and schedule start/stop) as JSON
envelopes. V1 is notifications-only — filters are set at connect time via
the events and channels query parameters and the server pushes envelopes;
the client does not send commands on this channel.
version: 0.1.0
defaultContentType: application/json
channels:
stream:
address: /_mock/stream
title: Management notification stream
description: |
A single consumer connects at upgrade time (WebSocket). The server pushes
zero or more JSON envelopes; each envelope's type discriminates its
payload. An omitted filter parameter matches everything; '*' is a glob.
parameters:
events:
description: Comma-separated event-name globs to subscribe to (omit for all)
channels:
description: Comma-separated channel-address globs to subscribe to (omit for all)
bindings:
ws:
method: GET
messages:
EventNotification:
$ref: '#/components/messages/EventNotification'
PushNotification:
$ref: '#/components/messages/PushNotification'
ConsumerNotification:
$ref: '#/components/messages/ConsumerNotification'
ScheduleNotification:
$ref: '#/components/messages/ScheduleNotification'
operations:
receiveNotifications:
action: receive
channel:
$ref: '#/channels/stream'
messages:
- $ref: '#/channels/stream/messages/EventNotification'
- $ref: '#/channels/stream/messages/PushNotification'
- $ref: '#/channels/stream/messages/ConsumerNotification'
- $ref: '#/channels/stream/messages/ScheduleNotification'
components:
messages:
EventNotification:
name: event
summary: Event notification — fired when a named event fires (spec-triggered or via POST /_mock/events)
contentType: application/json
payload:
$ref: '#/components/schemas/EventEnvelope'
PushNotification:
name: push
summary: Push notification — fired when a management push delivers a message to a channel
contentType: application/json
payload:
$ref: '#/components/schemas/PushEnvelope'
ConsumerNotification:
name: consumer
summary: Consumer lifecycle notification — fired when a consumer connects to or disconnects from a channel
contentType: application/json
payload:
$ref: '#/components/schemas/ConsumerEnvelope'
ScheduleNotification:
name: schedule
summary: Schedule notification — fired when a periodic message example starts or stops
contentType: application/json
payload:
$ref: '#/components/schemas/ScheduleEnvelope'
schemas:
EventEnvelope:
type: object
required:
- type
properties:
type:
type: string
const: event
ts:
type: integer
format: int64
event:
$ref: '#/components/schemas/EventEnvelopeBody'
EventEnvelopeBody:
type: object
properties:
name:
type: string
description: The named event (or built-in kind) that fired
schema:
type: string
description: Schema scope of the fire (empty for global)
global:
type: boolean
default: false
payload:
type: object
additionalProperties: true
description: Event payload exposed to templates via {$event.*}
PushEnvelope:
type: object
required:
- type
properties:
type:
type: string
const: push
ts:
type: integer
format: int64
push:
$ref: '#/components/schemas/PushEnvelopeBody'
PushEnvelopeBody:
type: object
properties:
channel:
type: string
description: Channel address the message was pushed to
connectionId:
type: string
description: Target connection when the push was targeted
payload:
type: object
additionalProperties: true
description: The delivered message payload
ConsumerEnvelope:
type: object
required:
- type
properties:
type:
type: string
const: consumer
ts:
type: integer
format: int64
consumer:
$ref: '#/components/schemas/ConsumerEnvelopeBody'
ConsumerEnvelopeBody:
type: object
properties:
action:
type: string
enum: [connected, disconnected]
connectionId:
type: string
channel:
type: string
streams:
type: array
items:
type: object
additionalProperties: true
description: Open SignalR streams for the connection
ScheduleEnvelope:
type: object
required:
- type
properties:
type:
type: string
const: schedule
ts:
type: integer
format: int64
schedule:
$ref: '#/components/schemas/ScheduleEnvelopeBody'
ScheduleEnvelopeBody:
type: object
properties:
action:
type: string
enum: [started, stopped]
exampleId:
type: string
channel:
type: string
interval:
type: integer
description: Delivery interval in milliseconds
Loading
Loading