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
14 changes: 8 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,27 @@ 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
- Protocol-neutral async management prefix `/_mock/async/{push,consumers,disconnect}`
- 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)
- Single event resource `POST /_mock/events` with a `type` discriminator (V1: `fire`)
- 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
- Recurring delivery moved off the schedule endpoint onto `interval` on `/_mock/examples`
- `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

### Removed
- Deprecated alias endpoints `POST /_mock/ws/push`, `GET /_mock/ws/consumers`, `POST /_mock/ws/disconnect` and `POST /_mock/events/fire`; the canonical `/_mock/async/*` and `POST /_mock/events` surface is the only way to reach those behaviors and any legacy `/_mock/ws/*` path answers a plain 404
- The schedule 410 stubs `POST /_mock/ws/schedule` and `DELETE /_mock/ws/schedule/{pushId}`
- The `x-send-events` extension mapping shim — a message example is classified solely by its `x-mock-match`/`x-mock-interval`/reply trigger; a spec still carrying the key loads without error and the key is silently ignored

### 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
Expand All @@ -38,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ The server exposes a control HTTP API under the `/_mock` prefix. Full schema: [a
- `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

See [cli.md](./cli.md) for the complete CLI specification.
Expand Down
168 changes: 0 additions & 168 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,26 +133,6 @@ paths:
$ref: '#/components/responses/InvalidRequest'
'500':
$ref: '#/components/responses/InternalError'
/events/fire:
post:
operationId: fireEventLegacy
summary: Fire a named event (deprecated alias)
description: |
Deprecated alias of POST /_mock/events. Accepts the legacy body without
the `type` discriminator (defaults to "fire"). Kept for backward
compatibility; use /_mock/events.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LegacyFireEventRequest'
deprecated: true
responses:
'200':
description: Event accepted
'400':
$ref: '#/components/responses/InvalidRequest'
/async/push:
post:
operationId: pushToChannel
Expand Down Expand Up @@ -253,101 +233,6 @@ paths:
HTTP response body)
'405':
$ref: '#/components/responses/NotUpgraded'
/ws/push:
post:
operationId: pushToChannelLegacy
summary: Push a message (deprecated alias)
deprecated: true
description: Deprecated alias of POST /_mock/async/push.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PushRequest'
responses:
'200':
description: Push accepted
'400':
$ref: '#/components/responses/InvalidRequest'
'404':
$ref: '#/components/responses/NotFound'
/ws/consumers:
get:
operationId: listConsumersLegacy
summary: List connected consumers (deprecated alias)
deprecated: true
description: Deprecated alias of GET /_mock/async/consumers.
parameters:
- name: channel
in: query
required: false
description: Channel address (omit for all channels)
schema:
type: string
responses:
'200':
description: Consumers listed
content:
application/json:
schema:
$ref: '#/components/schemas/ConsumersResponse'
/ws/schedule:
post:
operationId: scheduleRecurringPush
summary: Schedule a recurring push (removed)
description: |
Removed. Recurring delivery is expressed via the interval field on
POST /_mock/examples for an AsyncAPI target. This path answers 410 Gone.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ScheduleRequest'
responses:
'410':
$ref: '#/components/responses/GoneExamples'
/ws/schedule/{pushId}:
delete:
operationId: stopRecurringPush
summary: Stop a recurring push (removed)
description: |
Removed. Recurring delivery is stopped with DELETE /_mock/examples/{id}.
This path answers 410 Gone.
parameters:
- name: pushId
in: path
required: true
description: Push ID returned by the former schedule endpoint
schema:
type: string
responses:
'410':
$ref: '#/components/responses/GoneDelete'
/ws/disconnect:
post:
operationId: disconnectConsumerLegacy
summary: Force-disconnect a consumer (deprecated alias)
deprecated: true
description: Deprecated alias of POST /_mock/async/disconnect.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DisconnectRequest'
responses:
'200':
description: Consumer disconnected
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncActionResponse'
'400':
$ref: '#/components/responses/InvalidRequest'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Error:
Expand Down Expand Up @@ -538,31 +423,6 @@ components:
type: boolean
default: false
description: When true, the event is broadcast over all loaded schemas
LegacyFireEventRequest:
type: object
required:
- event
properties:
type:
type: string
enum: [fire]
default: fire
description: Optional discriminator; the legacy alias defaults to "fire"
event:
type: string
description: The named event to fire
payload:
type: object
description: Event payload exposed to consuming templates via {$event.*}
delay:
type: integer
minimum: 0
default: 0
description: Delivery delay in milliseconds
global:
type: boolean
default: false
description: When true, the event is broadcast over all loaded schemas
PushRequest:
type: object
required:
Expand All @@ -582,22 +442,6 @@ components:
minimum: 0
default: 0
description: Delivery delay in milliseconds
ScheduleRequest:
type: object
required:
- channel
- interval
properties:
channel:
type: string
description: AsyncAPI channel address to push to
interval:
type: integer
minimum: 1
description: Delivery interval in milliseconds
payload:
type: object
description: Message payload pushed at each interval
ManageEnvelope:
type: object
required:
Expand Down Expand Up @@ -735,15 +579,3 @@ components:
application/json:
schema:
$ref: '#/components/schemas/Error'
GoneExamples:
description: Removed — use POST /_mock/examples with interval
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
GoneDelete:
description: Removed — use DELETE /_mock/examples/{exampleId}
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
5 changes: 2 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ flowchart LR
- `x-mock-headers` - Custom response headers
- `x-mock-interval` / `x-mock-delay` - Periodically driven / delayed example timing
- `x-event-trigger` - Fire a named event from an OpenAPI example
- `x-send-events` - Legacy AsyncAPI event subscription mapping (deprecated shim)
- **Functions**:
- `ExtractSetState()`, `ExtractParamsMatch()`, `ExtractHeaders()`, `ExtractEventTriggers()`
- `EvaluateParamsMatch()` - Uses Runtime.Evaluator for expression evaluation
Expand Down Expand Up @@ -347,9 +346,9 @@ OASMock autodetects AsyncAPI 3.0.0/3.1.0 files (root key `asyncapi`, version maj
- **Neutral document view** (`internal/asyncapi/`): channels, operations, messages, bindings, examples with `x-mock-*` extensions; root `x-signalr` capture. The vendored `benelser/go-asyncapi` parser is isolated behind this package.
- **Protocol adapters** (`internal/server/protocol.go`): `ProtocolAdapter` strategies registered keyed by protocol. `httpAdapter` reuses the HTTP pipeline; `wsAdapter` upgrades WebSockets and tracks a connection registry.
- **SignalR overlay** (`internal/server/signalr_hub.go`): a document with root `x-signalr` is served as one SignalR hub — `POST {hubPath}/negotiate`, token-correlated ws upgrade, `\x1e` framing, handshake. Streams map to channels (`StreamInvocation` → held-open `StreamItem`); one-shot invocations map to operations (`Invocation` → `Completion`); event-driven items append into open streams via the open-stream registry.
- **Event broker** (`internal/server/event_broker.go`): OpenAPI examples fire named events via `x-event-trigger`; AsyncAPI message examples are classified at load (event-driven via `{$event.*}` `x-mock-match`, periodically driven via `x-mock-interval`, or reply) and registered keyed by match identity + schema scope. Classification is strict and atomic: mixed match contexts, an interval alongside any `x-mock-match`, a non-literal event identity, or fractional timing values are load errors, and a failed schema never partially registers. Legacy `x-send-events` entries map through a deprecation shim. Delivery runs the shared selection pipeline against the event context and narrows to per-connection recipients via a two-phase `{$connection.*}` partition (broadcast fast path otherwise). `POST /_mock/events` (with a `type` discriminator) fires events ad-hoc; built-ins `connect`/`receive` are actually fired from ws/SignalR lifecycle and inbound hooks gated by a cheap `hasSubscribers` check.
- **Event broker** (`internal/server/event_broker.go`): OpenAPI examples fire named events via `x-event-trigger`; AsyncAPI message examples are classified at load (event-driven via `{$event.*}` `x-mock-match`, periodically driven via `x-mock-interval`, or reply) and registered keyed by match identity + schema scope. Classification is strict and atomic: mixed match contexts, an interval alongside any `x-mock-match`, a non-literal event identity, or fractional timing values are load errors, and a failed schema never partially registers. Delivery runs the shared selection pipeline against the event context and narrows to per-connection recipients via a two-phase `{$connection.*}` partition (broadcast fast path otherwise). `POST /_mock/events` (with a `type` discriminator) fires events ad-hoc; built-ins `connect`/`receive` are actually fired from ws/SignalR lifecycle and inbound hooks gated by a cheap `hasSubscribers` check.
- **Scheduler** (`internal/server/job_scheduler.go`): per-example `{id, interval, deliver func()}` interval jobs drive periodically driven examples, with per-delivery templating against current state/env; shutdown cancels all jobs, `DELETE /_mock/examples/{id}` cancels individual ones.
- **Async mocking management API**: `/_mock/async/{push,consumers,disconnect}` (canonical, protocol-neutral prefix; legacy `/_mock/ws/*` kept as deprecated aliases), `/_mock/events` (type-discriminated fire), `/_mock/examples` (unified sync/async injection with strict oneOf validation and runtime `match`/`interval`/`delay`, plus `DELETE /_mock/examples/{id}`), `/_mock/stream` (management WebSocket notifications). The removed `/_mock/ws/schedule*` surface answers `410 Gone` pointing at the examples endpoint.
- **Async mocking management API**: `/_mock/async/{push,consumers,disconnect}` (canonical, protocol-neutral prefix), `/_mock/events` (type-discriminated fire), `/_mock/examples` (unified sync/async injection with strict oneOf validation and runtime `match`/`interval`/`delay`, plus `DELETE /_mock/examples/{id}`), `/_mock/stream` (management WebSocket notifications). The removed `/_mock/ws/*` and `/_mock/events/fire` surface answers plain 404 like any unknown route.
- **Management stream** (`internal/server/manage_ws.go`): `/_mock/stream` upgrade handler with connect-time `events`/`channels` filters; pushes `event`/`push`/`consumer`/`schedule` envelopes from the eventBus observer, consumer lifecycle hooks and scheduler start/stop. V1 is notifications-only (pings/pongs keep the socket alive).
- **Templating parity**: `{$message.*}`, `{$channel.*}`, `{$event.*}` data sources; `ExampleValue` wrapper unifies selection across OpenAPI/AsyncAPI; state/history integration uses each schema's isolated namespace.
- **Unsupported protocols** (`amqp`, `kafka`, ...) fail startup with exit code 3.
Expand Down
11 changes: 0 additions & 11 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,6 @@ examples:

Timing values are integer milliseconds: a fractional value (e.g. `x-mock-interval: 2.5`) is rejected at load instead of being silently truncated. Periodically driven examples honor `x-mock-skip` like every other example and are not emitted while it is set.

### x-send-events (deprecated)

**Location**: AsyncAPI message example object

**Deprecated**: kept for one release with a loader mapping shim. Each entry is translated to the unified form during loading with a verbose-mode deprecation note:

- `{on: <name>}` → `x-mock-match: {'{$event.name}': <name>}`
- `{on: connect, wait: N}` → `x-mock-match: {'{$event.name}': connect}` + `x-mock-delay: N`
- `{on: receive}` → `x-mock-match: {'{$event.name}': receive}`
- `{on: cron, wait: N}` → `x-mock-interval: N`

### Runtime matches and timing (management API)

`POST /_mock/examples` mirrors the extensions for AsyncAPI targets with `match`, `interval` and `delay` fields; the same classification and delivery rules apply. See `api/openapi.yaml`.
Expand Down
17 changes: 6 additions & 11 deletions internal/asyncapi/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,12 @@ func TestParse_NoRootSignalR(t *testing.T) {
}

/*
Scenario: Capturing x-send-events on a message example
Given an AsyncAPI message example with an x-send-events extension
Scenario: Capturing x-* extensions on a message example
Given an AsyncAPI message example with an arbitrary vendor extension
When Parse is called
Then the neutral example view surfaces the extension

Related spec scenarios: RS.EVT.7, RS.EVT.9, RS.EVT.10
Then the neutral example view surfaces the extension generically under x-*
*/
func TestParse_MessageExampleSendEvents(t *testing.T) {
func TestParse_MessageExampleVendorExtensions(t *testing.T) {
t.Parallel()

data := []byte(`
Expand All @@ -293,10 +291,7 @@ channels:
- name: ex1
payload:
level: info
x-send-events:
- on: orderCreated
wait: 50
- on: connect
x-mock-once: true
operations:
receiveAlerts:
action: receive
Expand All @@ -309,5 +304,5 @@ operations:
require.Len(t, doc.Channels[0].Messages, 1)
examples := doc.Channels[0].Messages[0].Examples
require.Len(t, examples, 1)
assert.Contains(t, examples[0].Extensions, "x-send-events")
assert.Contains(t, examples[0].Extensions, "x-mock-once")
}
4 changes: 2 additions & 2 deletions internal/runtime/event_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Given an event payload map and a nested value
When the EventSource is queried
Then the payload fields resolve via {$event.*}

Related spec scenarios: RS.EVT.8, RS.ATM.17
Related spec scenarios: RS.EVT.23, RS.ATM.17
*/
func TestEventSource_Get(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -41,7 +41,7 @@ Given an evaluator with an event source registered as "event"
When an expression is evaluated
Then the event payload value is returned

Related spec scenarios: RS.EVT.8
Related spec scenarios: RS.EVT.23
*/
func TestEvaluator_EventExpression(t *testing.T) {
t.Parallel()
Expand Down
Loading
Loading