From cfc639642010adfeb4701c084d1e6b45ecda4d68 Mon Sep 17 00:00:00 2001 From: Andrew Tereshko Date: Sat, 8 Aug 2026 14:16:49 +0300 Subject: [PATCH] Documentation cleanup & minor fixes --- AGENTS.md | 34 +- README.md | 92 ++- cmd/oasmock/mock.go | 19 - cmd/oasmock/mock_test.go | 48 +- docs/architecture.md | 935 ++++++++++++------------------- docs/ci-cd.md | 162 +++--- docs/cli.md | 32 +- docs/extensions.md | 75 ++- docs/json-rpc.md | 6 +- docs/project.md | 18 +- openspec/specs/cli/spec.md | 18 +- openspec/specs/json-rpc/spec.md | 8 +- test/cli/cli_integration_test.go | 56 +- 13 files changed, 529 insertions(+), 974 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ff3b2b9..2e68eed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,17 +3,17 @@ **Agent SHOULD NOT change this file, only suggest changes to user when inconsistency or potential improvement can be done** ## References -- [Project-specific standarts](docs/project.md) -- [Arhitecture docs](docs/architecture.md) +- [Project-specific standards](docs/project.md) +- [Architecture docs](docs/architecture.md) - [Project specs (BDD)](openspec/specs/) ## Development Guidelines -- Always read project standarts and architecture when new session started -- Cognitive Complexity ([metric by Sonar Source](https://redirect.sonarsource.com/doc/cognitive-complexity.html)) MUST be as low as possible by keeping conditionals simple and nesting levels moderately low (with helper functions and\or declarative approach) +- Always read project standards and architecture when new session started +- Cognitive Complexity ([metric by Sonar Source](https://redirect.sonarsource.com/doc/cognitive-complexity.html)) MUST be as low as possible by keeping conditionals simple and nesting levels moderately low (with helper functions and/or declarative approach) - Module coupling MUST be moderately low to enable clean unit testing and make codebase resilient to changes -- Module cohesion (module context, knoledge and logix dencity) MUST be as high as possible -- Code duplication SHOULD be as minimal as possible as long as it's reduces complexity (see the rule about coupling and cohesion) -- Function length SHOULD be ignored, as long no code or logic duplication is presented and code resposibility in the right place (high cohesion) +- Module cohesion (module context, knowledge and logic density) MUST be as high as possible +- Code duplication SHOULD be as minimal as possible as long as it reduces complexity (see the rule about coupling and cohesion) +- Function length SHOULD be ignored, as long as no code or logic duplication is present and code responsibility is in the right place (high cohesion) - Data-driven approaches SHOULD be used instead of repetitive control structures (declarative over imperative) - Core constants or configuration MUST be defined in one place, and derived representations (e.g., a set for fast lookup) SHOULD be derived programmatically. - When in need to perform frequent membership checks, source-of-truth slice SHOULD be converted into a map (set) once—preferably at initialization (init). @@ -21,15 +21,15 @@ ## Code Design - Use design-first and TDD principle: 1. Design function interface according to usage need and check it's usability in context - 2. Write or edit tests for parent code (code where new interface is used), mocking new\edited interface, to ensure host code works as expected - 3. Write or edir tests for interface itself + 2. Write or edit tests for parent code (code where new interface is used), mocking new/edited interface, to ensure host code works as expected + 3. Write or edit tests for interface itself 4. Write implementation of interface until tests will pass ## Quality Assurance Guidelines - All tests MUST follow common development guidelines - All test functions MUST contain multiline (/**/) comment before function declaration with: - Gherkin notation of test case - - List of related requirement scenario codes from opencode/spec at separate line + - List of related requirement scenario codes from openspec/specs at separate line example: ``` @@ -43,16 +43,4 @@ */ ``` - Use parameterized tests when the all test’s steps (AAA) are identical across all cases, and only the input and expected output differ. Otherwise, write separate tests. -- All tests are divided into "unit" and "integration" -- Benchmarks can be unit or integrative, and MUST comply with the corresponding rules -- **Unit tests** - checking one interface at the time - - MUST call one interface per test exclusively - - All dependencies including public interfaces calls within project codebase MUST be mocked or stubbed - - Private interfaces SHOULD NOT be tested directly, although they coverage MUST be implemented indirectly - - MUST be placed near tested module - - SHOULD use parralel execution when conflicts completely impossible - - SHOULD contain one assertion (or one logical group of assertions) per test -- **Integration tests** - checks ready-to-ship application as a complete system - - MUST check gaps in unit test cases and system integration result - - SHOULD NOT call any internal interfaces directly (only bundled system as black box) - - MUST be placed at test/ or it's subdirectories +- Unit tests and integration tests placement and conventions are defined in [project standards](docs/project.md#testing-standarts) diff --git a/README.md b/README.md index a3ed57f..13a0708 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,19 @@ ![spec coverage](https://img.shields.io/badge/spec_coverage-100%25-brightgreen) ![Go](https://img.shields.io/badge/go-1.23+-00ADD8) -A Go‑based mock server that leverages OpenAPI 3.0 schemas enhanced with custom extensions for conditional examples, state management, and runtime expressions. +A Go‑based mock server that leverages OpenAPI 3.0 schemas enhanced with custom extensions for conditional examples, state management, runtime expressions, and JSON-RPC 2.0 support. ## Features - Loads one or more OpenAPI 3.0 YAML/JSON files (with optional path prefixes) -- Supports custom extensions (`x‑mock‑params‑match`, `x‑mock‑skip`, `x‑mock‑once`, `x‑mock‑set‑state`, `x‑mock‑headers`) +- Supports custom extensions (`x‑mock‑match`, `x‑mock‑skip`, `x‑mock‑once`, `x‑mock‑set‑state`, `x‑mock‑headers`; legacy `x‑mock‑params‑match` alias still supported) - Runtime expressions (`{$request.path.id}`, `{$state.counter}`, `{$env.VAR}`) with modifiers (`default`, `getByPath`, `toJWT`) - In‑memory state per namespace (get/set/increment/delete) - Request history ring buffer with filtering via management API - Dynamic example injection at runtime via HTTP API - Configurable request delay, CORS, verbose logging -- Single binary, zero dependencies +- Single static binary, no runtime dependencies +- JSON‑RPC 2.0 gateway via `x‑rpc` extension (batch requests, notifications) ## Installation @@ -70,83 +71,48 @@ curl http://localhost:8080/hello ## OpenAPI Extensions -OASMock adds several custom extensions to OpenAPI example objects. Full documentation is available in [extensions.md](./extensions.md). +OASMock adds several custom extensions to OpenAPI example objects. Full reference: [extensions.md](./docs/extensions.md). -### `x‑mock‑params‑match` +### Match Conditions (`x‑mock‑match`) -Selects the example when the request matches the given conditions. +Selects the example when the request matches the given conditions (deprecated alias: `x‑mock‑params‑match`). ```yaml examples: admin: - x‑mock‑params‑match: + x‑mock‑match: '{$request.header.role}': admin value: message: Welcome, admin! ``` -### `x‑mock‑skip` +### Other Extensions -Skips the example (useful for temporarily disabling an example). +| Extension | Purpose | +|---|---| +| `x‑mock‑skip` | Temporarily exclude an example | +| `x‑mock‑once` | One‑time example (removed after first match) | +| `x‑mock‑set‑state` | Update server‑side state (supports `increment`, `value`, `null` for delete) | +| `x‑mock‑headers` | Set response headers (runtime expressions in values) | -### `x‑mock‑once` +### JSON‑RPC Gateway (`x‑rpc`) -Makes the example one‑time only (removed after first match). - -### `x‑mock‑set‑state` - -Updates server‑side state that can be referenced later via `{$state.*}`. - -```yaml -x‑mock‑set‑state: - counter: - increment: 1 - 'user-{$request.path.id}': - value: '{$request.body.name}' -``` - -### `x‑mock‑headers` - -Sets response headers (supports runtime expressions in values). +Route calls by body field instead of URL path. See [json-rpc.md](./docs/json-rpc.md). ## Runtime Expressions -Runtime expressions are enclosed in `{$...}` and can appear in extension keys, values, and response bodies. - -### Data Sources - -- `{$request.path.param}` -- `{$request.query.param}` -- `{$request.header.name}` -- `{$request.cookie.name}` -- `{$request.body.field}` -- `{$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}`. -### Modifiers +Modifiers: `\|default:value` (fallback), `\|getByPath:path` (traverse nested objects), `\|toJWT` (stub). -- `{$request.query.id|default:unknown}` – provides a default value if the expression cannot be resolved -- `{$state.object|getByPath:deep.nested.value}` – traverses an object -- `{$state.payload|toJWT}` – (stub) encodes the value as a JWT - -Embedded expressions are supported: - -```yaml -value: - url: "/api/users/{$request.path.id}/profile" -``` +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. - -### `GET /_mock/requests` +The server exposes a control HTTP API under the `/_mock` prefix. Full schema: [api/openapi.yaml](./api/openapi.yaml). -Retrieves the request history (optionally filtered by path, method, time range, etc.). - -### `POST /_mock/examples` - -Adds a dynamic example to an existing route. The request body follows the schema defined in [openapi.yaml](./api/openapi.yaml). +- `GET /_mock/requests` — request history (filterable by path, method, time range, pagination) +- `POST /_mock/examples` — add a dynamic example to an existing route ## Command‑Line Interface @@ -190,6 +156,16 @@ go test ./... golangci-lint run ``` +## Further Reading + +- [CLI reference](./docs/cli.md) — all flags, env vars, config file (`.oasmock.yaml`) +- [Extensions & runtime expressions](./docs/extensions.md) — full `x‑mock‑*` / `x‑rpc` reference +- [JSON‑RPC 2.0](./docs/json-rpc.md) — protocol details, batch support, error codes +- [Architecture](./docs/architecture.md) — component diagrams, interfaces, data flows +- [CI/CD](./docs/ci-cd.md) — pipeline, quality gates, release process +- [Project standards](./docs/project.md) — tech stack, conventions, testing, coverage policy +- [Specifications (BDD)](./openspec/specs/) — requirement scenarios + ## License -MIT \ No newline at end of file +MIT diff --git a/cmd/oasmock/mock.go b/cmd/oasmock/mock.go index d7beb77..e800120 100644 --- a/cmd/oasmock/mock.go +++ b/cmd/oasmock/mock.go @@ -61,25 +61,8 @@ func parseSchemaConfig(cmd *cobra.Command) error { return nil } - schemaVal := viper.Get("schema") schemasVal := viper.Get("schemas") - // Check mutual exclusivity - if schemaVal != nil && schemasVal != nil { - return validationError("cannot specify both 'schema' and 'schemas' in config file") - } - - // Handle single schema - if schemaVal != nil { - schema, ok := schemaVal.(string) - if !ok { - return validationError("'schema' must be a string") - } - config.sources = []string{schema} - config.prefixes = []string{} - return nil - } - // Handle schemas list if schemasVal != nil { schemas, ok := schemasVal.([]any) @@ -162,8 +145,6 @@ func init() { _ = viper.BindPFlag("nocors", mockCmd.Flags().Lookup("nocors")) _ = viper.BindPFlag("history_size", mockCmd.Flags().Lookup("history-size")) _ = viper.BindPFlag("no_control_api", mockCmd.Flags().Lookup("no-control-api")) - _ = viper.BindPFlag("from", mockCmd.Flags().Lookup("from")) - _ = viper.BindPFlag("prefix", mockCmd.Flags().Lookup("prefix")) } func runMock(cmd *cobra.Command, args []string) error { diff --git a/cmd/oasmock/mock_test.go b/cmd/oasmock/mock_test.go index 3f55703..d54d3ec 100644 --- a/cmd/oasmock/mock_test.go +++ b/cmd/oasmock/mock_test.go @@ -161,7 +161,7 @@ Given various YAML configuration inputs (valid and invalid) When parseSchemaConfig is called Then it should parse valid configurations correctly and return appropriate errors for invalid ones -Related spec scenarios: RS.CLI.19, RS.CLI.26, RS.CLI.27 +Related spec scenarios: RS.CLI.19, RS.CLI.27 */ func TestParseSchemaConfig(t *testing.T) { @@ -184,18 +184,6 @@ func TestParseSchemaConfig(t *testing.T) { assert.Nil(t, config.prefixes) }, }, - { - name: "single schema", - setup: func(cmd *cobra.Command) { - viper.Reset() - config = mockConfig{} - viper.Set("schema", "custom.yaml") - }, - check: func(t *testing.T) { - assert.Equal(t, []string{"custom.yaml"}, config.sources) - assert.Equal(t, []string{}, config.prefixes) - }, - }, { name: "multiple schemas with mixed formats", setup: func(cmd *cobra.Command) { @@ -211,16 +199,6 @@ func TestParseSchemaConfig(t *testing.T) { assert.Equal(t, []string{"/v1", ""}, config.prefixes) }, }, - { - name: "both schema and schemas present", - setup: func(cmd *cobra.Command) { - viper.Reset() - config = mockConfig{} - viper.Set("schema", "single.yaml") - viper.Set("schemas", []any{"multi.yaml"}) - }, - expectError: true, - }, { name: "invalid schemas element", setup: func(cmd *cobra.Command) { @@ -230,15 +208,6 @@ func TestParseSchemaConfig(t *testing.T) { }, expectError: true, }, - { - name: "invalid schema type", - setup: func(cmd *cobra.Command) { - viper.Reset() - config = mockConfig{} - viper.Set("schema", 123) - }, - expectError: true, - }, { name: "schemas object missing src", setup: func(cmd *cobra.Command) { @@ -287,8 +256,7 @@ func TestValidYAMLStructure(t *testing.T) { viper.Reset() config = mockConfig{} - yamlConfig := `schema: test.yaml -port: 8080 + yamlConfig := `port: 8080 delay: 500 verbose: true nocors: true @@ -299,7 +267,6 @@ no-control-api: true` require.NoError(t, viper.ReadConfig(bytes.NewBufferString(yamlConfig))) // Verify viper can read all keys - assert.Equal(t, "test.yaml", viper.GetString("schema")) assert.Equal(t, 8080, viper.GetInt("port")) assert.Equal(t, 500, viper.GetInt("delay")) assert.Equal(t, true, viper.GetBool("verbose")) @@ -331,7 +298,7 @@ Given configuration values defined in multiple sources (CLI flags, environment v When the configuration is resolved Then values from higher precedence sources override those from lower precedence sources -Related spec scenarios: RS.CLI.22, RS.CLI.23, RS.CLI.28, RS.CLI.29 +Related spec scenarios: RS.CLI.22, RS.CLI.23, RS.CLI.29 */ func TestConfigPrecedence(t *testing.T) { // t.Parallel() - cannot use with t.Setenv @@ -375,11 +342,12 @@ func TestConfigPrecedence(t *testing.T) { assert.Equal(t, 7070, port, "environment variable should override config file") }) - t.Run("CLI from flag overrides YAML schema", func(t *testing.T) { + t.Run("CLI from flag overrides YAML schemas", func(t *testing.T) { viper.Reset() config = mockConfig{} - // Simulate config file with schema - yamlConfig := `schema: custom.yaml` + // Simulate config file with schemas + yamlConfig := `schemas: + - custom.yaml` viper.SetConfigType("yaml") require.NoError(t, viper.ReadConfig(bytes.NewBufferString(yamlConfig))) // Create command with --from flag set @@ -395,7 +363,7 @@ func TestConfigPrecedence(t *testing.T) { // Call parseSchemaConfig err := parseSchemaConfig(cmd) require.NoError(t, err) - // Should keep flag value, not config file schema + // Should keep flag value, not config file schemas assert.Equal(t, []string{"flag.yaml"}, config.sources) assert.Equal(t, []string{}, config.prefixes) }) diff --git a/docs/architecture.md b/docs/architecture.md index 306db0d..cbbf51d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,32 +1,52 @@ # OASMock Architecture Documentation ## Table of Contents -1. [Overview](#overview) - - [Diagram Conventions](#diagram-conventions) -2. [Component Architecture](#1-component-architecture) -3. [Component Details](#2-component-details) - - [2.1 CLI Component](#21-cli-component-cmdoasmock) - - [2.2 Server Component](#22-server-component-internalserver) - - [2.3 Runtime Component](#23-runtime-component-internalruntime) - - [2.4 Extensions Component](#24-extensions-component-internalextensions) - - [2.5 Loader Component](#25-loader-component-internalloader) - - [2.6 State Component](#26-state-component-internalstate) - - [2.7 History Component](#27-history-component-internalhistory) - - [2.8 Mock Component](#28-mock-component-mock) -4. [Sequence Flows](#3-sequence-flows) - - [3.1 CLI Initialization Flow](#31-cli-initialization-flow) - - [3.2 HTTP Mock Request Flow](#32-http-mock-request-flow) - - [3.3 Management API - Dynamic Example Addition](#33-management-api---dynamic-example-addition) -5. [Interface Definitions](#4-interface-definitions) - - [4.1 Server Interfaces](#41-server-interfaces-internalserverinterfacesgo) - - [4.2 Runtime Interfaces](#42-runtime-interfaces-internalruntimeexpressiongo) - - [4.3 Extension Functions](#43-extension-functions-internalextractionsextractgo) -6. [Data Flow Summary](#5-data-flow-summary) -7. [Design Patterns](#6-design-patterns) -8. [Testing Architecture](#7-testing-architecture) -9. [Extension Points](#8-extension-points) -10. [Performance Considerations](#9-performance-considerations) -11. [Conclusion](#conclusion) +- [OASMock Architecture Documentation](#oasmock-architecture-documentation) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Diagram Conventions](#diagram-conventions) + - [1. Component Architecture](#1-component-architecture) + - [2. Component Details](#2-component-details) + - [2.1 CLI Component (`cmd/oasmock/`)](#21-cli-component-cmdoasmock) + - [2.2 Server Component (`internal/server/`)](#22-server-component-internalserver) + - [2.3 Runtime Component (`internal/runtime/`)](#23-runtime-component-internalruntime) + - [2.4 Extensions Component (`internal/extensions/`)](#24-extensions-component-internalextensions) + - [2.5 Loader Component (`internal/loader/`)](#25-loader-component-internalloader) + - [2.6 State Component (`internal/state/`)](#26-state-component-internalstate) + - [2.7 History Component (`internal/history/`)](#27-history-component-internalhistory) + - [2.8 Mock Component (`mock/`)](#28-mock-component-mock) + - [3. Sequence Flows](#3-sequence-flows) + - [3.1 CLI Initialization Flow](#31-cli-initialization-flow) + - [3.2 HTTP Mock Request Flow](#32-http-mock-request-flow) + - [3.3 Management API - Dynamic Example Addition](#33-management-api---dynamic-example-addition) + - [4. Interface Definitions](#4-interface-definitions) + - [4.1 Server Interfaces (`internal/server/interfaces.go`)](#41-server-interfaces-internalserverinterfacesgo) + - [4.2 Runtime Interfaces (`internal/runtime/expression.go`)](#42-runtime-interfaces-internalruntimeexpressiongo) + - [4.3 Extension Functions (`internal/extensions/extract.go`)](#43-extension-functions-internalextensionsextractgo) + - [5. Data Flow Summary](#5-data-flow-summary) + - [5.1 Initialization Flow](#51-initialization-flow) + - [5.2 Request Handling Flow](#52-request-handling-flow) + - [5.3 State Management Flow](#53-state-management-flow) + - [5.4 History Tracking Flow](#54-history-tracking-flow) + - [5.5 Dynamic Example Flow](#55-dynamic-example-flow) + - [6. Design Patterns](#6-design-patterns) + - [6.1 Dependency Injection](#61-dependency-injection) + - [6.2 Adapter Pattern](#62-adapter-pattern) + - [6.3 Factory Pattern](#63-factory-pattern) + - [6.4 Strategy Pattern](#64-strategy-pattern) + - [6.5 Observer Pattern](#65-observer-pattern) + - [7. Testing Architecture](#7-testing-architecture) + - [7.1 Mock Generation](#71-mock-generation) + - [7.2 Unit Testing Strategy](#72-unit-testing-strategy) + - [7.3 Integration Testing](#73-integration-testing) + - [8. Extension Points](#8-extension-points) + - [8.1 Custom Data Sources](#81-custom-data-sources) + - [8.2 Custom Extension Processing](#82-custom-extension-processing) + - [8.3 Custom State/History Stores](#83-custom-statehistory-stores) + - [9. Performance Considerations](#9-performance-considerations) + - [9.1 Memory Management](#91-memory-management) + - [9.2 Concurrency](#92-concurrency) + - [9.3 Runtime Evaluation](#93-runtime-evaluation) ## Overview @@ -34,225 +54,83 @@ OASMock is an OpenAPI-based mock server with a modular architecture built in Go. ### Diagram Conventions -All PlantUML diagrams in this document follow the guidelines from the [plantuml-creator skill](../.opencode/skills/plantuml-creator/SKILL.md) to ensure consistency and maintainability: +All diagrams in this document use Mermaid syntax for broad compatibility across Git SaaS platforms and IDEs: -- **Source annotations**: Each diagram element that corresponds to actual code includes a source reference in the format `/'source: @/path/to/file.go:line'/` linking to the relevant file and line number. -- **Modern styling**: Diagrams use the modern ` - -title 🏗️ OASMock Component Architecture - -package "🎯 Presentation Layer" #E8F5E9 { - /'source: @/cmd/oasmock'/ - [CLI] <> as CLI - /'source: @/cmd/oasmock'/ - note right of CLI - **Responsibilities**: - - Parse command-line arguments - - Load configuration - - Start server - - Handle graceful shutdown - **Key Files**: - - cmd/oasmock/root.go - - cmd/oasmock/mock.go - end note -} - -package "⚙️ Application Layer" #E3F2FD { - /'source: @/internal/server'/ - [Server\n(internal/server/)] <> as Server - /'source: @/internal/server/server.go:101'/ - note right of Server - **Responsibilities**: - - HTTP server management - - Request routing - - Response generation - - Component coordination - **Key Interfaces**: - - RouteProvider - - StateStore - - HistoryStore - - ExpressionEvaluator - - ExtensionProcessor - end note -} - -package "🔧 Domain Layer" #F3E5F5 { - /'source: @/internal'/ - [Runtime\n(internal/runtime/)] <> as Runtime - /'source: @/internal/runtime/expression.go:180'/ - note right of Runtime - **Responsibilities**: - - Runtime expression evaluation - - Data source abstraction - **Key Interfaces**: - - DataSource - - Evaluator - **Implementations**: - - RequestSource - - StateSource - - EnvSource - end note - - [Extensions\n(internal/extensions/)] <> as Extensions - /'source: @/internal/extensions/extract.go:20'/ - note right of Extensions - **Responsibilities**: - - Process OpenAPI extensions - - x-mock-* extension handling - **Functions**: - - ExtractSetState() - - ExtractParamsMatch() - - EvaluateParamsMatch() - - ExtractHeaders() - end note -} - -package "🛠️ Infrastructure Layer" #FFF8E1 { - /'source: @/internal'/ - [Loader\n(internal/loader/)] <> as Loader - /'source: @/internal/loader/schema.go:20'/ - note right of Loader - **Responsibilities**: - - Load OpenAPI schemas - - Build route mappings - - Path pattern conversion - **Key Types**: - - SchemaInfo - - RouteMapping - end note - - [State\n(internal/state/)] <> as State - /'source: @/internal/state/state.go:20'/ - note right of State - **Responsibilities**: - - Namespaced key-value storage - - Thread-safe operations - **Key Type**: - - Manager - **Operations**: - - Get/Set/Increment/Delete - - GetNamespace/GetAll - end note - - [History\n(internal/history/)] <> as History - /'source: @/internal/history/history.go:20'/ - note right of History - **Responsibilities**: - - Request/response history - - Ring buffer storage - **Key Types**: - - RequestRecord - - ResponseRecord - - RingBuffer - end note - - [Mock\n(mock/)] <> as Mock - /'source: @/mock'/ - note right of Mock - **Responsibilities**: - - Generated interface mocks - - Unit testing support - **Packages**: - - mock_runtime - - mock_server - **Generated via**: - - go:generate mockgen - end note -} - -' Dependency Relationships -CLI --> Loader : Load schemas -CLI --> Server : Start server - -Server --> Loader : RouteProvider.BuildRouteMappings() -Server --> Runtime : ExpressionEvaluator, DataSource factories -Server --> Extensions : ExtensionProcessor -Server --> State : StateStore operations -Server --> History : HistoryStore operations - -Extensions --> Runtime : Uses Evaluator for param matching - -Mock ..> Server : Mocks all server interfaces -Mock ..> Runtime : Mocks runtime interfaces - -legend right - | Color | Layer | - | Light Green | Presentation | - | Light Blue | Application | - | Light Purple | Domain | - | Light Yellow | Infrastructure | -endlegend - -caption Figure 1: High-level component architecture - -@enduml +```mermaid +flowchart LR + subgraph PRES["🎯 Presentation Layer"] + CLI["CLI
cmd/oasmock/"] + end + subgraph APP["⚙️ Application Layer"] + Server["Server
internal/server/"] + end + subgraph DOM["🔧 Domain Layer"] + Runtime["Runtime
internal/runtime/"] + Extensions["Extensions
internal/extensions/"] + end + subgraph INF["🛠️ Infrastructure Layer"] + Loader["Loader
internal/loader/"] + State["State
internal/state/"] + History["History
internal/history/"] + Mock["Mock
mock/"] + end + subgraph LEGEND["Legend"] + direction LR + L1["Presentation"] + L2["Application"] + L3["Domain"] + L4["Infrastructure"] + end + + CLI -->|Load schemas| Loader + CLI -->|Start server| Server + + Server -->|"RouteProvider.BuildRouteMappings()"| Loader + Server -->|ExpressionEvaluator, DataSource factories| Runtime + Server -->|ExtensionProcessor| Extensions + Server -->|StateStore operations| State + Server -->|HistoryStore operations| History + + Extensions -->|Uses Evaluator for param matching| Runtime + + Mock -.->|Mocks all server interfaces| Server + Mock -.->|Mocks runtime interfaces| Runtime + + classDef green fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20 + classDef blue fill:#E3F2FD,stroke:#1565C0,color:#0D47A1 + classDef purple fill:#F3E5F5,stroke:#7B1FA2,color:#4A148C + classDef orange fill:#FFF3E0,stroke:#F57C00,color:#E65100 + classDef yellow fill:#FFF8E1,stroke:#FF8F00,color:#FF6F00 + classDef pink fill:#FFEBEE,stroke:#C2185B,color:#880E4F + classDef teal fill:#E0F2F1,stroke:#00897B,color:#004D40 + classDef gray fill:#F5F5F5,stroke:#616161,color:#424242 + classDef leg fill:none,stroke:#ccc,color:#424242 + + class CLI green + class Server blue + class Runtime purple + class Extensions orange + class Loader yellow + class State pink + class History teal + class Mock gray + class L1 green + class L2 blue + class L3 purple + class L4 yellow ``` +*Figure 1: High-level component architecture* + ## 2. Component Details ### 2.1 CLI Component (`cmd/oasmock/`) @@ -274,8 +152,14 @@ caption Figure 1: High-level component architecture - **Key Files**: - `server.go` - Main server implementation and HTTP handlers - `interfaces.go` - All public interfaces and dependency definitions - - `server_management.go` - Management API endpoints - - `wrappers.go` - Adapter implementations + - `server_management.go` - Management API endpoints + - `server_example.go` - Example selection and response generation + - `server_eval.go` - Runtime expression evaluation integration + - `server_state.go` - State management helpers + - `jsonrpc.go` - JSON-RPC handler (gateway requests) + - `jsonrpc_protocol.go` - JSON-RPC 2.0 protocol parsing and error responses + - `wrappers.go` - Adapter implementations + - `adapters/` - Formal adapter layer for external components - **Public Interfaces**: - `RouteProvider` - Builds route mappings from OpenAPI schemas - `StateStore` - Manages namespaced state with CRUD operations @@ -289,7 +173,8 @@ caption Figure 1: High-level component architecture - Response generation and example selection - Runtime expression evaluation coordination - Extension processing and state updates - - Management API endpoints (`/_mock/examples`, `/_mock/requests`) + - Management API endpoints (`/_mock/examples`, `/_mock/requests`) + - RPC gateway dispatch (JSON-RPC to operation mapping via `x-rpc`) - **Dependencies**: Loader, Runtime, Extensions, State, History ### 2.3 Runtime Component (`internal/runtime/`) @@ -318,7 +203,7 @@ caption Figure 1: High-level component architecture - `x-mock-set-state` - Set server state after response - `x-mock-skip` - Skip example from selection - `x-mock-once` - Use example only once - - `x-mock-params-match` - Conditional example selection + - `x-mock-match` - Conditional example selection (legacy alias: `x-mock-params-match`) - `x-mock-headers` - Custom response headers - **Functions**: - `ExtractSetState()`, `ExtractParamsMatch()`, `ExtractHeaders()` @@ -388,7 +273,24 @@ caption Figure 1: High-level component architecture - Thread-safe concurrent access - **Dependencies**: None (self-contained) -### 2.8 Mock Component (`mock/`) +### 2.8 RPC Protocol Subsystem (`internal/server/` and `internal/loader/`) +**Purpose**: JSON-RPC 2.0 gateway enabling procedure dispatch by body field instead of URL path. +- **Key Files**: + - `internal/server/jsonrpc.go` - RPC handler (batch support, notification handling) + - `internal/server/jsonrpc_protocol.go` - JSON-RPC 2.0 protocol (parse, error responses) + - `internal/loader/rpc.go` - `x-rpc` extension parsing + - `internal/loader/rpc_config.go` - RPC configuration types +- **Key Interfaces**: + - `RpcProtocol` - `ParseBody()`, `ErrorResponse()`, `ContentType()` + - `RpcCall` - Parsed call representation (Procedure, ID, Raw body) +- **Responsibilities**: + - Parse batch and single JSON-RPC request bodies + - Dispatch each call through the example selection pipeline + - Handle notifications (no response entry) + - Generate standard JSON-RPC 2.0 error responses +- **Dependencies**: Extensions (for x-mock-* processing), Runtime (expression evaluation) + +### 2.9 Mock Component (`mock/`) **Purpose**: Generated interface mocks for unit testing. - **Packages**: - `mock_runtime` - Mocks for runtime interfaces (`DataSource`, `Evaluator`) @@ -407,114 +309,68 @@ caption Figure 1: High-level component architecture ### 3.1 CLI Initialization Flow -```plantuml -@startuml cli-init-sequence-grouped -!theme cerulean -autonumber "[000]" - - - -title 🌱 CLI Initialization Flow - Component Grouped - -actor "👤 User" as User - -box "🎯 CLI Component" #E8F5E9 /'source: @/cmd/oasmock'/ - participant "🖥️ CLI\n(cmd/oasmock)" as CLI /'source: @/cmd/oasmock/root.go:46'/ -end box - -box "🛠️ Loader Component" #FFF8E1 /'source: @/internal/loader'/ - participant "📚 loader.LoadSchemas" as Loader /'source: @/internal/loader/schema.go:20'/ -end box - -box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/ - participant "🏗️ server.New" as ServerNew /'source: @/internal/server/server.go:101'/ - participant "🔧 RouteProvider" as RouteProvider /'source: @/internal/server/interfaces.go:13'/ - participant "🚀 Server.Start" as ServerStart /'source: @/internal/server/server.go:447'/ - participant "🌐 http.Server" as HTTPServer -end box - -box "💾 Infrastructure Components" #E0F2F1 /'source: @/internal'/ - participant "💾 StateStore" as StateStore /'source: @/internal/server/interfaces.go:37'/ - participant "📜 HistoryStore" as HistoryStore /'source: @/internal/server/interfaces.go:55'/ - participant "⚙️ Runtime Factories" as RuntimeFactories /'source: @/internal/server/interfaces.go:97'/ -end box - -== Configuration Phase == - -User -> CLI ++ : Execute `oasmock mock --from ...` -CLI -> CLI : Parse flags & config (viper) -CLI --> User : Validate configuration - -== Schema Loading == - -CLI -> Loader ++ : LoadSchemas(sources, prefixes) -loop for each source - Loader -> Loader : loadSingleSchema() - Loader -> Loader : openapi3.NewLoader().LoadFromData() - Loader -> Loader : spec.Validate() -end -Loader --> CLI -- : []loader.SchemaInfo - -== Server Initialization == - -CLI -> ServerNew ++ : New(config, schemas) -ServerNew -> ServerNew : Convert schemas to server.SchemaInfo -ServerNew -> ServerNew : Create default dependencies - -ServerNew -> RouteProvider ++ : BuildRouteMappings(schemas) -RouteProvider -> RouteProvider : Process OpenAPI paths/operations -RouteProvider --> ServerNew -- : []server.RouteMapping - -ServerNew -> StateStore : Initialize state manager -ServerNew -> HistoryStore : Initialize ring buffer -ServerNew -> RuntimeFactories : Create data source factories - -ServerNew -> ServerNew : Initialize routeMap, onceExamples -ServerNew -> ServerNew : setupRouter() with middleware -ServerNew --> CLI -- : *Server instance - -== Server Startup == - -CLI -> ServerStart ++ : Start() (goroutine) -ServerStart -> HTTPServer ++ : ListenAndServe() -HTTPServer --> ServerStart -- : Listening on port -ServerStart --> CLI -- : Server running - -CLI -> CLI : Wait for interrupt signal -CLI --> User -- : Server ready message - -caption Figure 2: CLI startup sequence - -@enduml +```mermaid +sequenceDiagram + autonumber + + actor User as 👤 User + box 🎯 CLI Component + participant CLI as 🖥️ CLI
(cmd/oasmock) + end + box 🛠️ Loader Component + participant Loader as 📚 loader.LoadSchemas + end + box ⚙️ Server Component + participant ServerNew as 🏗️ server.New + participant RouteProvider as 🔧 RouteProvider + participant ServerStart as 🚀 Server.Start + participant HTTPServer as 🌐 http.Server + end + box 💾 Infrastructure Components + participant StateStore as 💾 StateStore + participant HistoryStore as 📜 HistoryStore + participant RuntimeFactories as ⚙️ Runtime Factories + end + + Note over User,CLI: === Configuration Phase === + User->>+CLI: Execute `oasmock mock --from ...` + CLI->>CLI: Parse flags & config (viper) + CLI-->>User: Validate configuration + + Note over CLI,Loader: === Schema Loading === + CLI->>+Loader: LoadSchemas(sources, prefixes) + loop for each source + Loader->>Loader: loadSingleSchema() + Loader->>Loader: openapi3.NewLoader().LoadFromData() + Loader->>Loader: spec.Validate() + end + Loader-->>-CLI: []loader.SchemaInfo + + Note over CLI,ServerNew: === Server Initialization === + CLI->>+ServerNew: New(config, schemas) + ServerNew->>ServerNew: Convert schemas to server.SchemaInfo + ServerNew->>ServerNew: Create default dependencies + ServerNew->>+RouteProvider: BuildRouteMappings(schemas) + RouteProvider->>RouteProvider: Process OpenAPI paths/operations + RouteProvider-->>-ServerNew: []server.RouteMapping + ServerNew->>StateStore: Initialize state manager + ServerNew->>HistoryStore: Initialize ring buffer + ServerNew->>RuntimeFactories: Create data source factories + ServerNew->>ServerNew: Initialize routeMap, onceExamples + ServerNew->>ServerNew: setupRouter() with middleware + ServerNew-->>-CLI: *Server instance + + Note over CLI,HTTPServer: === Server Startup === + CLI->>+ServerStart: Start() (goroutine) + ServerStart->>+HTTPServer: ListenAndServe() + HTTPServer-->>-ServerStart: Listening on port + ServerStart-->>-CLI: Server running + CLI->>CLI: Wait for interrupt signal + CLI-->>-User: Server ready message ``` +*Figure 2: CLI startup sequence* + **Description**: 1. **User Interaction**: CLI component parses command-line arguments and validates configuration 2. **Schema Loading**: Loader component loads and validates OpenAPI schemas from files @@ -524,124 +380,73 @@ caption Figure 2: CLI startup sequence ### 3.2 HTTP Mock Request Flow -```plantuml -@startuml http-mock-request-sequence-grouped -!theme bluegray -autonumber "[000]" - - - -title 🌐 HTTP Mock Request Flow - Component Grouped - -actor "👤 HTTP Client" as Client - -box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/ - participant "🛣️ Server Router" as Router /'source: @/internal/server/server.go:286'/ - entity "📍 RouteMapping" as Mapping /'source: @/internal/server/interfaces.go:19'/ - participant "📄 Response Generator" as ResponseGen /'source: @/internal/server/server_example.go:95'/ -end box - -box "🔧 Runtime Component" #F3E5F5 /'source: @/internal/runtime'/ - participant "⚡ Runtime.Evaluator" as Evaluator /'source: @/internal/runtime/expression.go:180'/ - participant "📤 RequestSource" as RequestSource /'source: @/internal/runtime/expression.go:77'/ - participant "💾 StateSource" as StateSource /'source: @/internal/runtime/expression.go:134'/ - participant "🌍 EnvSource" as EnvSource /'source: @/internal/runtime/expression.go:162'/ -end box - -box "🔌 Extensions Component" #FFF3E0 /'source: @/internal/extensions'/ - control "🔌 ExtensionProcessor" as ExtProcessor /'source: @/internal/server/interfaces.go:123'/ -end box - -== Request Reception & Middleware == - -Client -> Router ++ : GET /v1/users/123 -Router -> Router : Middleware stack execution - -== Route Lookup == - -Router -> Mapping ++ : Route lookup via routeKey() -Mapping --> Router -- : RouteMapping struct - -== Runtime Environment Setup == - -Router -> Evaluator ++ : runtime.NewEvaluator() -Evaluator -> RequestSource ++ : AddSource("request", source) -RequestSource -> RequestSource : Parse path/query/headers/body -RequestSource --> Evaluator -- : DataSource ready - -Evaluator -> StateSource ++ : AddSource("state", source) -StateSource -> StateSource : Get namespace data -StateSource --> Evaluator -- : DataSource ready - -Evaluator -> EnvSource ++ : AddSource("env", source) -EnvSource -> EnvSource : Read OS environment -EnvSource --> Evaluator -- : DataSource ready - -Evaluator --> Router -- : Configured evaluator - -== Response Selection & Processing == - -group Response Selection - Router -> Router : selectResponse(mapping, evaluator) - Router -> Router : selectMediaType(response) - Router -> Router : selectDynamicExample() / selectExample() -end - -group Extension Processing - Router -> ExtProcessor ++ : ExtractSetState(example) - ExtProcessor --> Router : map[string]any - - Router -> ExtProcessor : ExtractParamsMatch(example) - ExtProcessor --> Router : ParamsMatch - - Router -> ExtProcessor : EvaluateParamsMatch(params, evaluator) - ExtProcessor -> ExtProcessor : Evaluate runtime expressions - ExtProcessor --> Router -- : bool match result -end - -== Response Generation == - -Router -> ResponseGen ++ : generateResponse(example, evaluator) -ResponseGen -> ResponseGen : Evaluate runtime expressions -ResponseGen -> ResponseGen : Apply state updates via StateStore -ResponseGen --> Router -- : body, headers, statusCode - -== Final Response == - -Router -> Client : HTTP Response (200 OK) -deactivate Router - -caption Figure 3: HTTP request handling sequence - -@enduml +```mermaid +sequenceDiagram + autonumber + + actor Client as 👤 HTTP Client + box ⚙️ Server Component + participant Router as 🛣️ Server Router + participant Mapping as 📍 RouteMapping + participant ResponseGen as 📄 Response Generator + end + box 🔧 Runtime Component + participant Evaluator as ⚡ Runtime.Evaluator + participant RequestSource as 📤 RequestSource + participant StateSource as 💾 StateSource + participant EnvSource as 🌍 EnvSource + end + box 🔌 Extensions Component + participant ExtProcessor as 🔌 ExtensionProcessor + end + + Note over Client,Router: === Request Reception & Middleware === + Client->>+Router: GET /v1/users/123 + Router->>Router: Middleware stack execution + + Note over Router,Mapping: === Route Lookup === + Router->>+Mapping: Route lookup via routeKey() + Mapping-->>-Router: RouteMapping struct + + Note over Router,Evaluator: === Runtime Environment Setup === + Router->>+Evaluator: runtime.NewEvaluator() + Evaluator->>+RequestSource: AddSource("request", source) + RequestSource->>RequestSource: Parse path/query/headers/body + RequestSource-->>-Evaluator: DataSource ready + Evaluator->>+StateSource: AddSource("state", source) + StateSource->>StateSource: Get namespace data + StateSource-->>-Evaluator: DataSource ready + Evaluator->>+EnvSource: AddSource("env", source) + EnvSource->>EnvSource: Read OS environment + EnvSource-->>-Evaluator: DataSource ready + Evaluator-->>-Router: Configured evaluator + + Note over Router,Router: === Response Selection & Processing === + Router->>Router: selectResponse(mapping, evaluator) + Router->>Router: selectMediaType(response) + Router->>Router: selectDynamicExample() / selectExample() + + Router->>+ExtProcessor: ExtractSetState(example) + ExtProcessor-->>Router: map[string]any + Router->>ExtProcessor: ExtractParamsMatch(example) + ExtProcessor-->>Router: ParamsMatch + Router->>ExtProcessor: EvaluateParamsMatch(params, evaluator) + ExtProcessor->>ExtProcessor: Evaluate runtime expressions + ExtProcessor-->>-Router: bool match result + + Note over Router,ResponseGen: === Response Generation === + Router->>+ResponseGen: generateResponse(example, evaluator) + ResponseGen->>ResponseGen: Evaluate runtime expressions + ResponseGen->>ResponseGen: Apply state updates via StateStore + ResponseGen-->>-Router: body, headers, statusCode + + Note over Client,Router: === Final Response === + Router->>Client: HTTP Response (200 OK) + deactivate Router ``` +*Figure 3: HTTP request handling sequence* + **Description**: 1. **Request Reception**: Server component receives HTTP request and processes middleware 2. **Route Resolution**: RouteMapping lookup finds matching OpenAPI operation @@ -652,117 +457,64 @@ caption Figure 3: HTTP request handling sequence ### 3.3 Management API - Dynamic Example Addition -```plantuml -@startuml management-api-sequence-grouped -!theme bluegray -autonumber "[000]" - - - -title 🛠️ Management API - Dynamic Example Addition - -actor "👤 HTTP Client" as Client - -box "⚙️ Server Component" #E3F2FD /'source: @/internal/server'/ - participant "🛣️ Server Router" as Router /'source: @/internal/server/server.go:286'/ - control "✅ Request Validator" as Validator /'source: @/internal/server/server_management.go:50'/ - participant "📍 RouteMapping" as Mapping /'source: @/internal/server/interfaces.go:19'/ - database "➕ Dynamic Examples Store" as DynStore /'source: @/internal/server/server_management.go:177'/ - participant "📤 Response Builder" as ResponseBuilder /'source: @/internal/server/server.go:35'/ -end box - -== Request Reception == - -Client -> Router ++ : POST /_mock/examples\nContent-Type: application/json -Router -> Router : Parse JSON body - -== Request Validation == - -group Schema Validation - Router -> Validator ++ : validateAddExampleRequest(body) - Validator -> Validator : JSON schema validation (gojsonschema) - - alt Invalid Schema - Validator --> Router : Validation error - Router --> Client : 400 Bad Request\n{"error": "..."} +```mermaid +sequenceDiagram + autonumber + + actor Client as 👤 HTTP Client + box ⚙️ Server Component + participant Router as 🛣️ Server Router + participant Validator as ✅ Request Validator + participant Mapping as 📍 RouteMapping + participant DynStore as ➕ Dynamic Examples Store + participant ResponseBuilder as 📤 Response Builder + end + + Note over Client,Router: === Request Reception === + Client->>+Router: POST /_mock/examples
Content-Type: application/json + Router->>Router: Parse JSON body + + Note over Router,Validator: === Request Validation === + Router->>+Validator: validateAddExampleRequest(body) + Validator->>Validator: JSON schema validation (gojsonschema) + alt Invalid Schema + Validator-->>Router: Validation error + Router-->>Client: 400 Bad Request
{"error": "..."} + else Valid Schema + Validator-->>-Router: Validation passed + end + + Note over Router,Mapping: === Route Matching === + Router->>+Mapping: Find matching route
(Pattern, Method) + Mapping->>Mapping: Search through []RouteMapping + alt No Match Found + Mapping-->>Router: nil + Router-->>Client: 400 No matching route + else Match Found + Mapping-->>-Router: *RouteMapping + end + + Note over Router,DynStore: === Dynamic Example Creation === + Router->>+DynStore: Create dynamicExample struct + DynStore->>DynStore: Parse conditions, response + DynStore->>DynStore: Generate unique ID + DynStore-->>-Router: dynamicExample ready + + Note over Router,DynStore: === Storage Operation === + Router->>DynStore: Store under routeKey + DynStore->>DynStore: Append to examples slice + DynStore-->>Router: Success + + Note over Client,Router: === Success Response === + Router->>+ResponseBuilder: Build success response + ResponseBuilder->>ResponseBuilder: JSON encoding + ResponseBuilder-->>-Router: Success message + Router-->>Client: 200 OK
{"success": true, "id": "dynex-...", "message": "Example added"} deactivate Router - - else Valid Schema - Validator --> Router -- : Validation passed - end -end - -== Route Matching == - -Router -> Mapping ++ : Find matching route\n(Pattern, Method) -Mapping -> Mapping : Search through []RouteMapping - -alt No Match Found - Mapping --> Router : nil - Router --> Client : 400 No matching route - deactivate Router - -else Match Found - Mapping --> Router -- : *RouteMapping -end - -== Dynamic Example Creation == - -Router -> DynStore ++ : Create dynamicExample struct -DynStore -> DynStore : Parse conditions, response -DynStore -> DynStore : Generate unique ID -DynStore --> Router -- : dynamicExample ready - -== Storage Operation == - -Router -> DynStore : Store under routeKey -DynStore -> DynStore : Append to examples slice -DynStore --> Router : Success - -== Success Response == - -Router -> ResponseBuilder ++ : Build success response -ResponseBuilder -> ResponseBuilder : JSON encoding -ResponseBuilder --> Router -- : Success message -Router --> Client : 200 OK\n{"success": true, "id": "dynex-...", "message": "Example added"} -deactivate Router - -caption Figure 4: Dynamic example addition via management API - -@enduml ``` +*Figure 4: Dynamic example addition via management API* + **Description**: 1. **API Request**: Client sends POST request to management API endpoint 2. **Request Validation**: Server validates JSON payload against schema @@ -871,7 +623,7 @@ type EnvSource struct { ### 4.3 Extension Functions (`internal/extensions/extract.go`) ```go -// ExtractParamsMatch extracts the x-mock-params-match extension +// ExtractParamsMatch extracts the x-mock-match (or x-mock-params-match) extension from an example. func ExtractParamsMatch(ex *openapi3.Example) (ParamsMatch, bool) // ExtractSkip extracts x-mock-skip extension @@ -886,7 +638,8 @@ func ExtractSetState(ex *openapi3.Example) (map[string]any, bool) // ExtractHeaders extracts x-mock-headers extension func ExtractHeaders(ex *openapi3.Example) (map[string]any, bool) -// EvaluateParamsMatch evaluates parameter matching conditions +// EvaluateParamsMatch evaluates parameter matching conditions. +// The ParamsMatch type is an alias for map[string]any (defined in extensions/match.go). func EvaluateParamsMatch(pm ParamsMatch, eval runtime.Evaluator) (bool, error) ``` @@ -920,6 +673,13 @@ Management API request → Validation → Route matching → Create dynamic exam Store in Server → Future requests can use dynamic example ``` +### 5.6 JSON-RPC Flow +``` +JSON-RPC request → RpcProtocol.ParseBody → Per-call dispatch (batch) → +RouteMapping lookup by procedure name → Expression evaluation → +Example selection → State / history / response collection +``` + ## 6. Design Patterns ### 6.1 Dependency Injection @@ -938,9 +698,10 @@ Store in Server → Future requests can use dynamic example - Different data sources and evaluators can be plugged in - Extension processing strategies for different x-mock-* extensions -### 6.5 Observer Pattern -- History tracks all requests/responses -- State updates observable through management API +### 6.5 History Tracking / State Exposure +- History ring buffer tracks all requests/responses for later inspection +- State is queryable through management API endpoints +- Middleware-based recording pattern decouples tracking from business logic ## 7. Testing Architecture @@ -973,6 +734,10 @@ Store in Server → Future requests can use dynamic example - Implement `StateStore` or `HistoryStore` interfaces - Replace default implementations via `Dependencies` +### 8.4 Custom RPC Protocols +- Implement `RpcProtocol` interface (`ParseBody`, `ErrorResponse`, `ContentType`) +- Configure via `x-rpc.protocolType` in the OpenAPI spec + ## 9. Performance Considerations ### 9.1 Memory Management @@ -988,17 +753,3 @@ Store in Server → Future requests can use dynamic example ### 9.3 Runtime Evaluation - Expression caching could be added for performance - Simple path parsing algorithm with O(n) complexity - -## Conclusion - -OASMock follows a clean, modular architecture with clear separation of concerns. The component-based design enables testability, maintainability, and extensibility. Key strengths include: - -1. **Modularity**: Clear component boundaries with defined interfaces -2. **Testability**: Dependency injection and mock generation support -3. **Extensibility**: Pluggable interfaces for custom implementations -4. **Performance**: Efficient algorithms and memory management -5. **Standards Compliance**: Full OpenAPI 3.0 support with extensions - -The architecture supports both static OpenAPI-based mocking and dynamic runtime behavior through extensions and the management API. - -The server also supports protocol-level routing via `x-rpc`: a single gateway endpoint dispatches requests by procedure name (extracted from the request body), reusing the same example selection, expression evaluation, and extension processing pipeline. See [JSON-RPC Documentation](json-rpc.md) for details. \ No newline at end of file diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 0340eff..c150932 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -14,55 +14,36 @@ The OASMock CI/CD pipeline is a unified GitHub Actions workflow that ensures cod ## Pipeline Architecture -```plantuml -@startuml -title OASMock CI/CD Pipeline -skinparam backgroundColor #F5F5F5 -skinparam activityBackgroundColor #FFFFFF -skinparam activityBorderColor #333333 -skinparam activityFontSize 14 -skinparam arrowColor #333333 - -start - -partition "Parallel Fast Checks" { - :Unit Tests & Coverage; - :Spec Coverage Check; -} - -partition "Build & Package" { - :Build Binaries; - note right - Cross-compile for: - - Linux (amd64) - - macOS (amd64) - - Windows (amd64) - end note - :Upload Artifacts; -} - -partition "Integration Testing" { - :Download Linux Binary; - :Run Integration Tests; -} - -partition "Release (Tags Only)" { - :Create GitHub Release; - :Publish npm Package; -} - -stop - -' Dependency arrows -Unit Tests & Coverage --> Build Binaries -Spec Coverage Check --> Build Binaries -Build Binaries --> Upload Artifacts -Upload Artifacts --> Download Linux Binary -Download Linux Binary --> Run Integration Tests -Run Integration Tests --> Create GitHub Release -Create GitHub Release --> Publish npm Package - -@enduml +```mermaid +flowchart TD + subgraph P1["Parallel Fast Checks"] + direction LR + A[Unit Tests & Coverage] + B[Spec Coverage Check] + end + + subgraph P2["Build & Package"] + C[Build Binaries
Cross-compile for:
• Linux amd64
• macOS amd64
• Windows amd64] + D[Upload Artifacts] + end + + subgraph P3["Integration Testing"] + E[Download Linux Binary] + F[Run Integration Tests] + end + + subgraph P4["Release Tags Only"] + G[Create GitHub Release] + H[Publish npm Package] + end + + A --> C + B --> C + C --> D + D --> E + E --> F + F --> G + G --> H ``` ## Trigger Events @@ -170,44 +151,39 @@ Create GitHub Release --> Publish npm Package ## Artifact Flow -```plantuml -@startuml -title Artifact Flow Through Pipeline -skinparam backgroundColor #F5F5F5 - -rectangle "Build Job" as build { - file "dist/oasmock-linux-amd64" as linux - file "dist/oasmock-darwin-amd64" as darwin - file "dist/oasmock-windows-amd64.exe" as windows -} - -database "GitHub Artifacts" as artifacts { - file "oasmock-binaries" as artifact -} - -rectangle "Integration Tests" as integration { - file "bin/oasmock-linux-amd64" as testbin -} - -rectangle "Release Job" as release { - file "dist/*" as releasebin -} - -cloud "GitHub Release" as github { - file "Release Assets" as assets -} - -cloud "npm Registry" as npm { - file "npm package" as npmpkg -} - -build --> artifacts : Upload -artifacts --> integration : Download (linux only) -artifacts --> release : Download (all) -release --> github : Create release with binaries -release --> npm : Publish package - -@enduml +```mermaid +flowchart LR + subgraph BUILD["Build Job"] + A[dist/oasmock-linux-amd64] + B[dist/oasmock-darwin-amd64] + C[dist/oasmock-windows-amd64.exe] + end + + subgraph ARTIFACTS["GitHub Artifacts"] + D[oasmock-binaries] + end + + subgraph INTEG["Integration Tests"] + E[bin/oasmock-linux-amd64] + end + + subgraph RELEASE["Release Job"] + F[dist/*] + end + + subgraph GH["GitHub Release"] + G[Release Assets] + end + + subgraph NPM["npm Registry"] + H[npm package] + end + + BUILD -->|Upload| ARTIFACTS + ARTIFACTS -->|Download linux only| INTEG + ARTIFACTS -->|Download all| RELEASE + RELEASE -->|Create release with binaries| GH + RELEASE -->|Publish package| NPM ``` ## Environment Variables @@ -272,15 +248,3 @@ To add support for a new platform (e.g., ARM64): - `scripts/check-coverage.sh` - Coverage check script - `scripts/analyze_scenario_coverage.py` - Spec coverage analysis - `test/_shared/binhelper/` - Binary helper for integration tests - -## Migration from Previous Workflows - -This unified pipeline replaces: -- `.github/workflows/go.yml` (build and test) -- `.github/workflows/release.yml` (release) - -The new pipeline provides: -- Better parallelism (unit tests + spec coverage) -- Single build artifact reused across jobs -- Consistent "test what you ship" approach -- Simplified maintenance with single workflow file \ No newline at end of file diff --git a/docs/cli.md b/docs/cli.md index c7852cf..0c40dea 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -17,16 +17,17 @@ oasmock [options] | `--from` | string | `src/openapi.yaml` | Source OpenAPI schema. Can be specified multiple times. | | `--prefix` | string | `''` | URI prefix for the schema. Can be specified for each `--from` parameter. | | `--port` | number | `19191` | Port to listen on. | -| `--delay` | number | `0` | Delay between request and response in milliseconds. | +| `--delay` | number | `100` | Delay between request and response in milliseconds. | | `--verbose` | boolean | `false` | Enable verbose logging. | | `--nocors` | boolean | `false` | Disable automatic CORS compliance. | | `--no-control-api` | boolean | `false` | Disable management HTTP API served at _mock/ | +| `--history-size` | number | `1000` | Maximum number of requests to keep in history. | | `--version`, `-v` | boolean | `false` | Show version information and exit. | | `--help`, `-h` | boolean | `false` | Show global help and exit. | ### Environment Variables -All values overridable by they cli options counterparts. +All values are overridable by their CLI option counterparts. | Variable | Description | |--------------------------|-------------------------------------| @@ -34,6 +35,7 @@ All values overridable by they cli options counterparts. | `OASMOCK_VERBOSE` | If `true`, enables verbose logging. | | `OASMOCK_NO_CORS` | If `true`, disables CORS. | | `OASMOCK_NO_CONTROL_API` | Disable management HTTP API. | +| `OASMOCK_HISTORY_SIZE` | Maximum request history size. | ### Configuration File @@ -49,25 +51,17 @@ The CLI can read configuration from a `.oasmock.yaml` file in the current workin | Key | Type | Description | |-------------------|---------------------|--------------------------------------------------------------------------| -| `schema` | string | Single OpenAPI schema path. Mutually exclusive with `schemas`. | | `schemas` | list | Multiple schemas, each either a string (path) or object with `src` and optional `prefix`. | | `port` | number | Port to listen on. | | `delay` | number | Delay between request and response in milliseconds. | | `verbose` | boolean | Enable verbose logging. | | `nocors` | boolean | Disable automatic CORS compliance. | -| `history-size` | number | Maximum number of requests to keep in history. | -| `no-control-api` | boolean | Disable the management control API. | +| `history_size` | number | Maximum number of requests to keep in history. | +| `no_control_api` | boolean | Disable the management control API. | **Examples:** -**Single schema:** -```yaml -schema: ../some/path/openapi.yaml -port: 8080 -verbose: true -``` - - **Multiple schemas with prefixes:** +**Multiple schemas with prefixes:** ```yaml schemas: - src: api/v1/openapi.yaml @@ -78,17 +72,6 @@ delay: 500 nocors: true ``` -**All options (single schema):** -```yaml -schema: api/openapi.yaml -port: 9090 -delay: 200 -verbose: true -nocors: false -history-size: 100 -no-control-api: false -``` - ### Examples **Start mock server with default params**: @@ -123,5 +106,4 @@ oasmock --nocors | 1 | General error | | 2 | Invalid command‑line arguments | | 3 | Schema loading or validation failed | -| 4 | Port already in use | diff --git a/docs/extensions.md b/docs/extensions.md index 0a8822f..f5b6ce2 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -1,6 +1,6 @@ # OASMock OpenAPI Extensions -This document describes the custom OpenAPI extensions used by the OpenApi Mock tool. +This document describes the custom OpenAPI extensions used by OASMock. ## x-mock-match @@ -12,7 +12,7 @@ This document describes the custom OpenAPI extensions used by the OpenApi Mock t ```yaml examples: first: - x-mock-params-match: + x-mock-match: '{$request.query.id}': 12 '{$request.query.limit}': type: number @@ -21,7 +21,7 @@ examples: # ... example data ``` -**Alias**: x-mock-params-match - deprecated, only for backward compatibility +**Alias**: x-mock-params-match — deprecated, kept for backward compatibility ## x-mock-skip @@ -41,7 +41,7 @@ examples: **Location**: OAS example object -**Purpose**: Sets server state that can be used later as a condition in `x-mock-params-match`. Runtime expressions are available in keys and values. +**Purpose**: Sets server state that can be used later as a condition in `x-mock-match`. Runtime expressions are available in keys and values. **Example**: ```yaml @@ -49,7 +49,7 @@ examples: first: x-mock-set-state: state-plain-key: '{$request.body.param}' - 'state-mixed-{$request.cookie.some}': plain value, + 'state-mixed-{$request.cookie.some}': plain value '{$request.cookie.some}': plain value state-obj-key: value: @@ -59,7 +59,7 @@ examples: deleted-state-key: null ``` -### x-mock-headers +## x-mock-headers **Location**: OAS example object @@ -76,7 +76,7 @@ examples: - 'cookie-name=second cookie;' ``` -### x-mock-once +## x-mock-once **Location**: OAS example object @@ -90,55 +90,46 @@ examples: # ... example data ``` -## Runtime Expressions - -Runtime expressions like `{$request.url}` are evaluated inside keys and values of mock extensions. Dot as part of a property name must be escaped: `{$request.cookie.dot\.dot}`. - -Value modifiers can be specified after a `|` sign. Example: `{$request.path.param|encodeURIComponent}`. - -### Custom Modifiers - -| Modifier | Description | Example | -|--------------|-----------------------------------------------------------------------------|---------------------------------------------------| -| `default` | Returns a default value if the provided data is empty. | `{$request.path.param\|default:some default value}` | -| `getByPath` | Returns part of an object or array by a dot‑separated path. | `{$state.someObject\|getByPath:some.example.array.last}` | -| `toJWT` | Packs the provided object into JWT format (expires in 1h, aud="mock‑client"). | `{$state.someObject\|toJWT}` | -| `getJWKn` | *(To be documented)* | | -| `getJWKe` | *(To be documented)* | | - -### Available Data - -| Expression example | Description | Value Example | -|-------------------------------|-----------------------------------------------------|------------------------------------------------| -| `$url` | Full request URL | `https://example.org/api/pathParamValue/?param=value` | -| `$method` | Request method | `POST` | -| `$request.path.param` | Path parameters (declared in routes) | `pathParamValue` | -| `$request.query.param` | Query string parameters | `value` | -| `$request.header.header-name` | Request headers | `application/json` | -| `$request.body.param` | Data from request body (JSON or form) | `some body data` | -| `$request.cookie.cookieName` | Parsed request cookies | `value from cookie` | -| `$state.someSavedParam` | State data (set previously with `x-mock-set-state`) | `param saved to state` | -| `$env.ENV_VAR` | Runtime environment variables | `value from env` | - ## x-rpc **Location**: OpenAPI document root **Purpose**: Configures an RPC gateway endpoint for routing calls by body field instead of URL path. Currently supports JSON-RPC 2.0. -**Example**: +**Minimal example**: ```yaml x-rpc: gateway: /rpc protocolType: json-rpc - procedure: - call: method - match: post.operationId ``` -When `x-rpc` is present, all POST operations under the gateway path are treated as RPC procedures. The procedure name is derived from the operation's `operationId`. Requests are dispatched by matching the `method` field in the JSON-RPC request body against the procedure name. +When `x-rpc` is present, all POST operations under the gateway path are treated as RPC procedures. The procedure name is derived from the field specified at `x-rpc.procedure.match`. Requests are dispatched by matching the field specified in `x-rpc.procedure.call` in the JSON-RPC request body against the procedure name. For JSON-RPC contexts, `{$request.body.id}`, `{$request.body.method}`, and `{$request.body.params.*}` expressions evaluate against the individual call object (not the batch array), enabling per-call resolution in batch requests. See [JSON-RPC Documentation](json-rpc.md) for full details. +# Runtime Expressions + +Runtime expressions like `{$request.path.id}` are evaluated inside keys and values of mock extensions. Dot as part of a property name must be escaped: `{$request.cookie.dot\.dot}`. + +Value modifiers can be specified after a `|` sign. Example: `{$request.path.param|default:not-found}`. + +### Custom Modifiers + +| Modifier | Description | Example | +|--------------|-----------------------------------------------------------------------------|---------------------------------------------------| +| `default` | Returns a default value if the provided data is empty. | `{$request.path.param\|default:some default value}` | +| `getByPath` | Returns part of an object or array by a dot‑separated path. | `{$state.someObject\|getByPath:some.example.array.last}` | +| `toJWT` | (stub) Returns a placeholder JWT‑like string. | `{$state.someObject\|toJWT}` | +### Available Data + +| Expression example | Description | +|----------------------------------------|-----------------------------------------------------| +| `{$request.path.param}` | Path parameters (declared in routes) | +| `{$request.query.param}` | Query string parameters | +| `{$request.header.header-name}` | Request headers | +| `{$request.body.param}` | Data from request body (JSON or form) | +| `{$request.cookie.cookieName}` | Parsed request cookies | +| `{$state.someSavedParam}` | State data (set previously with `x-mock-set-state`) | +| `{$env.ENV_VAR}` | Runtime environment variables | diff --git a/docs/json-rpc.md b/docs/json-rpc.md index bce2c5f..b1714aa 100644 --- a/docs/json-rpc.md +++ b/docs/json-rpc.md @@ -111,14 +111,14 @@ A single OpenAPI spec can contain both RPC procedures and normal HTTP routes. Pa Start the server with a spec containing `x-rpc`: ```bash -oasmock mock --schema spec.yaml +oasmock mock --from spec.yaml ``` With a schema prefix: ```bash -oasmock mock --schema spec.yaml --prefix /api +oasmock mock --from spec.yaml --prefix /api # Gateway available at POST /api/rpc ``` -All existing flags (`--port`, `--delay`, `--verbose`, `--cors`, `--history-size`, `--control-api`) work identically with RPC-enabled specs. +All existing flags (`--port`, `--delay`, `--verbose`, `--nocors`, `--history-size`, `--no-control-api`) work identically with RPC-enabled specs. diff --git a/docs/project.md b/docs/project.md index 2d49299..c6caf32 100644 --- a/docs/project.md +++ b/docs/project.md @@ -17,7 +17,7 @@ - `test/_shared` - Common files for tests codebase including fixtures, helper functions, resources etc - `test/_shared/resources` - Various resources (e.g. yaml, json files) - `docs/` - Project documentation - - `docs/diagrams` - PlantUML diagrams + - `docs/diagrams` - PlantUML diagrams (container for extracted `.puml` files) ## Conventions @@ -30,8 +30,20 @@ - Use Testify package for all test assertions - Define multiple test scenarios using slice of structs - Each test should be marked with list of requirement scenario codes from [openspec's specs](`openspec/specs`) -- Unit tests: place near tested module with `_test.go` suffix -- Integration tests: place under `test/` directory, skip when `testing.Short()` +- Benchmarks can be unit or integrative, and MUST comply with the corresponding rules +- **Unit tests** - checking one interface at the time + - SHOULD be placed near tested module with `_test.go` suffix + - MUST call one interface per test exclusively + - All dependencies including public interfaces calls within project codebase MUST be mocked or stubbed + - Private interfaces SHOULD NOT be tested directly, although they coverage MUST be implemented indirectly + - MUST be placed near tested module + - SHOULD use parralel execution when conflicts completely impossible + - SHOULD contain one assertion (or one logical group of assertions) per test +- **Integration tests** - checks ready-to-ship application as a complete system + - SHOULD be placed under `test/` directory, skip when `testing.Short()` + - MUST check gaps in unit test cases and system integration result + - SHOULD NOT call any internal interfaces directly (only bundled system as black box) + - MUST be placed at test/ or it's subdirectories ## Coverage Policy - **Code coverage**: Minimum threshold (currently 70%) that must not regress diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md index 634d9cf..e522550 100644 --- a/openspec/specs/cli/spec.md +++ b/openspec/specs/cli/spec.md @@ -87,7 +87,7 @@ The CLI SHALL return appropriate exit codes as defined in [cli.md](../../../../c - **THEN** the CLI exits with code 1 ### Requirement: Configuration file support -The CLI SHALL read configuration from a `.oasmock.yaml` file in the current working directory (or user home directory). The configuration file SHALL use simplified schema configuration keys: `schema` (single string) for one schema, `schemas` (list) for multiple schemas. Each element in `schemas` SHALL be either a string (schema path) or an object with `src` and optional `prefix`. Other options SHALL use kebab‑case keys matching CLI flag names (`port`, `delay`, `verbose`, `nocors`, `history‑size`, `no‑control‑api`). +The CLI SHALL read configuration from a `.oasmock.yaml` file in the current working directory (or user home directory). The configuration file SHALL use the `schemas` list key for schema configuration. Each element in `schemas` SHALL be either a string (schema path) or an object with `src` and optional `prefix`. Other options SHALL use kebab‑case keys matching CLI flag names (`port`, `delay`, `verbose`, `nocors`, `history‑size`, `no‑control‑api`). #### Scenario RS.CLI.19: Config file present with valid YAML - **WHEN** a `.oasmock.yaml` file exists in the current directory with valid YAML content @@ -109,14 +109,6 @@ The CLI SHALL read configuration from a `.oasmock.yaml` file in the current work - **WHEN** a configuration value is defined both in `.oasmock.yaml` and as an environment variable (e.g., `port: 8080` in YAML and `OASMOCK_PORT=7070`) - **THEN** the CLI uses the value from the environment variable (unless overridden by a CLI flag) -#### Scenario RS.CLI.24: Single schema configuration -- **WHEN** a `.oasmock.yaml` file contains: - ```yaml - schema: ../some/path/openapi.yaml - port: 8080 - ``` -- **THEN** the CLI loads the single schema from the specified path, as if `--from ../some/path/openapi.yaml` were given on the command line - #### Scenario RS.CLI.25: Multiple schemas configuration - **WHEN** a `.oasmock.yaml` file contains: ```yaml @@ -127,18 +119,10 @@ The CLI SHALL read configuration from a `.oasmock.yaml` file in the current work ``` - **THEN** the CLI loads both schemas, the first with prefix `/url/prefix` and the second without prefix, as if `--from ../some/path/openapi.yaml --prefix /url/prefix --from ../path/unprefixed.openapi.yaml` were given on the command line -#### Scenario RS.CLI.26: Invalid schema configuration (both schema and schemas) -- **WHEN** a `.oasmock.yaml` file contains both `schema` and `schemas` keys -- **THEN** the CLI reports an error and exits with code 2 (invalid command-line arguments) - #### Scenario RS.CLI.27: Invalid schemas list element - **WHEN** a `.oasmock.yaml` file contains a `schemas` list with an element that is neither a string nor an object with `src` - **THEN** the CLI reports an error and exits with code 2 (invalid command-line arguments) -#### Scenario RS.CLI.28: CLI flag overrides YAML schema configuration -- **WHEN** a `.oasmock.yaml` file contains `schema: path/to/schema.yaml` and the user runs `oasmock --from other.yaml` -- **THEN** the CLI loads `other.yaml` (ignoring the YAML schema configuration) - #### Scenario RS.CLI.29: CLI flag overrides YAML schemas configuration - **WHEN** a `.oasmock.yaml` file contains `schemas:` list with multiple schemas and the user runs `oasmock --from single.yaml` - **THEN** the CLI loads `single.yaml` (ignoring the YAML schemas configuration) diff --git a/openspec/specs/json-rpc/spec.md b/openspec/specs/json-rpc/spec.md index 173fbf1..8dacb00 100644 --- a/openspec/specs/json-rpc/spec.md +++ b/openspec/specs/json-rpc/spec.md @@ -54,8 +54,8 @@ The mock server SHALL parse incoming JSON-RPC request bodies according to the pr - **THEN** the server parses it as one call with procedure "subtract", params `{"a":10}`, id `1`, HasID=true #### Scenario RS.JRP.11: Batch parsing -- **WHEN** a POST body is an array of two JSON-RPC call objects -- **THEN** the server parses it into two calls with correct fields per element +- **WHEN** a POST body is an array of two or more JSON-RPC call objects +- **THEN** the server parses it into same number of calls with correct fields per element #### Scenario RS.JRP.12: Invalid JSON body - **WHEN** the POST body is not valid JSON @@ -92,8 +92,8 @@ The mock server SHALL route a single JSON-RPC call to the matching operation and The mock server SHALL process batch JSON-RPC calls (array body) and return an array of responses. #### Scenario RS.JRP.19: Batch processing -- **WHEN** a POST body is an array of two valid calls with different ids and methods -- **THEN** the response is a JSON array of two response envelopes with matching ids +- **WHEN** a POST body is an array of two or more valid calls with different ids and methods +- **THEN** the response is a JSON array of corresponding number of response envelopes with matching ids #### Scenario RS.JRP.20: Batch with mixed success and error - **WHEN** a batch contains one valid method and one unknown method diff --git a/test/cli/cli_integration_test.go b/test/cli/cli_integration_test.go index 431ca92..f3683c6 100644 --- a/test/cli/cli_integration_test.go +++ b/test/cli/cli_integration_test.go @@ -678,7 +678,7 @@ Scenario: Config file present with valid YAML When a .oasmock.yaml file exists in the current directory with valid YAML content Then the CLI uses the values from the file as defaults (unless overridden by environment variables or CLI flags) -Related spec scenarios: RS.CLI.19, RS.CLI.24 +Related spec scenarios: RS.CLI.19 */ func TestCLIConfigFilePresent(t *testing.T) { if testing.Short() { @@ -700,7 +700,8 @@ func TestCLIConfigFilePresent(t *testing.T) { // Create config file with custom port configContent := `port: 19999 verbose: true -schema: test.yaml` +schemas: + - test.yaml` require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file") // Copy test schema to current directory (relative path) schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml") @@ -891,7 +892,8 @@ func TestCLIPrecedenceFlagOverridesConfig(t *testing.T) { require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory") // Config file sets port 8080 configContent := `port: 8080 -schema: test.yaml` +schemas: + - test.yaml` require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file") // Copy test schema schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml") @@ -951,7 +953,8 @@ func TestCLIPrecedenceEnvOverridesConfig(t *testing.T) { require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory") // Config file sets port 8080 configContent := `port: 8080 -schema: test.yaml` +schemas: + - test.yaml` require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file") // Copy test schema schemaPath := filepath.Join(originalWd, "../../test/_shared/resources/test.yaml") @@ -1088,51 +1091,6 @@ verbose: true` _ = cmd.Wait() } -/* -Scenario: Invalid schema configuration (both schema and schemas) -When a .oasmock.yaml file contains both schema and schemas keys -Then the CLI reports an error and exits with code 2 - -Related spec scenarios: RS.CLI.26 -*/ -func TestCLIConfigInvalidBothSchemaAndSchemas(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - // Build the binary if not present - if _, err := os.Stat("../../bin/oasmock"); os.IsNotExist(err) { - t.Skip("binary not found, skipping integration test") - } - tmpDir := t.TempDir() - originalWd, err := os.Getwd() - require.NoError(t, err, "failed to get working directory") - binaryPath := filepath.Join(originalWd, "../../bin/oasmock") - defer os.Chdir(originalWd) //nolint:errcheck - - require.NoError(t, os.Chdir(tmpDir), "failed to change to temp directory") - // Config file with both schema and schemas (invalid) - configContent := `schema: single.yaml -schemas: - - multi.yaml -port: 8080` - require.NoError(t, os.WriteFile(".oasmock.yaml", []byte(configContent), 0644), "failed to write config file") - // Create a dummy schema file - require.NoError(t, os.WriteFile("single.yaml", []byte("dummy"), 0644), "failed to write dummy schema") - - // Run oasmock - should exit with error code 2 - cmd := exec.Command(binaryPath, "mock") - output, err := cmd.CombinedOutput() - require.Error(t, err, "command should fail with invalid config") - // Check exit code (2 = invalid arguments) - exitErr, ok := err.(*exec.ExitError) - require.True(t, ok, "error should be ExitError") - expectedCode := 2 // invalid command-line arguments - assert.Equal(t, expectedCode, exitErr.ExitCode(), "expected exit code %d for invalid config, got %d", expectedCode, exitErr.ExitCode()) - // Error message should indicate the problem - outputStr := string(output) - assert.Contains(t, outputStr, "cannot specify both 'schema' and 'schemas'", "error message missing expected content: %s", outputStr) -} - /* Scenario: CLI integration test location When CLI integration tests are written