mockstack 0.13.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mockstack-0.13.0 → mockstack-0.14.0}/.env.example +11 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.gitignore +1 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/CLAUDE.md +11 -5
- {mockstack-0.13.0 → mockstack-0.14.0}/Dockerfile +1 -1
- {mockstack-0.13.0 → mockstack-0.14.0}/PKG-INFO +15 -2
- {mockstack-0.13.0 → mockstack-0.14.0}/README.md +14 -1
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/configuration.md +14 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/guides/proxyrules-cookbook.md +124 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/proxyrules.md +209 -9
- mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/rate-limited.json.j2 +1 -0
- mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/unavailable.json.j2 +1 -0
- mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/orders/created.json.j2 +1 -0
- mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/rules.yml +36 -0
- mockstack-0.14.0/examples/proxyrules-cookbook/09-recording/rules.yml +11 -0
- mockstack-0.14.0/examples/proxyrules-cookbook/09-recording/scrubbers.py +14 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/README.md +7 -1
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/.env.example +5 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/config.py +24 -1
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/constants.py +26 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/display.py +16 -1
- mockstack-0.14.0/mockstack/recording.py +77 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/rules.py +93 -4
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/filefixtures.py +1 -2
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/proxyrules.py +281 -39
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/conftest.py +11 -3
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_cookbook.py +110 -1
- mockstack-0.14.0/mockstack/tests/live/test_fixture_status_live.py +73 -0
- mockstack-0.14.0/mockstack/tests/live/test_record_mode_live.py +142 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/conftest.py +22 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_proxyrules.py +326 -9
- mockstack-0.14.0/mockstack/tests/strategies/test_proxyrules_record.py +552 -0
- mockstack-0.14.0/mockstack/tests/test_config.py +168 -0
- mockstack-0.14.0/mockstack/tests/test_display.py +51 -0
- mockstack-0.14.0/mockstack/tests/test_recording.py +176 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_rules.py +143 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/PKG-INFO +15 -2
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/SOURCES.txt +12 -1
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/scm_file_list.json +11 -0
- mockstack-0.14.0/mockstack.egg-info/scm_version.json +8 -0
- mockstack-0.13.0/mockstack/tests/test_config.py +0 -58
- mockstack-0.13.0/mockstack/tests/test_display.py +0 -20
- mockstack-0.13.0/mockstack.egg-info/scm_version.json +0 -8
- {mockstack-0.13.0 → mockstack-0.14.0}/.dockerignore +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/ci.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/publish-docs.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/smoke-test-pypi.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/.pre-commit-config.yaml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/CODE_OF_CONDUCT.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/CONTRIBUTING.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/LICENSE +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/SECURITY.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docker-entrypoint.sh +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/favicon.ico +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/logo.png +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/mockstack.png +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/ollama.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/base.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/filefixtures.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/.env.example +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/README.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/templates/servicename2-api-v2-user.533ec889-7c68-45c8-b21e-4a7e455d1234.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/README.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/mockstack-langchain-example.ipynb +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/ollama-openai-v1-chat-completions.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/openai-v1-chat-completions.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/01-tagged-traffic/fixtures/projects/project.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/01-tagged-traffic/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/fixtures/archived/projects/project.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/fixtures/healthy/projects/project.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/03-sql-gateway/fixtures/analytics/sales_facts.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/03-sql-gateway/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/04-json-literals/fixtures/orders/search.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/04-json-literals/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/fixtures/users/archived.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/fixtures/users/page.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixture_assertions.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/orders/echo.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/projects/project.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/users/user-1.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/07-redirect-mode/fixtures/users/user-1.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/07-redirect-mode/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/upstream.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/README.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/fixtures/healthy/analytics/sales_facts.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/fixtures/healthy/projects/project.json.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/upstream.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/.env.example +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/README.md +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/rules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mkdocs.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/exceptions.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/identifiers.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/intent.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/lifespan.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/llm/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/llm/ollama.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/main.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/middleware.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/catchall.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/homepage.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/base.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/create_mixin.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/factory.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/telemetry.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/templating.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/conftest.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/templates/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_example_eval_isolation.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_predicates_live.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_reverse_proxy_live.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/llm/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/llm/test_ollama.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/test_catchall.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/test_homepage.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/__init__.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_create_mixin.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_filefixtures_example.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_docs_settings.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_docs_yaml.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_identifiers.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_intent.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_middleware.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_telemetry.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_templating.py +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/dependency_links.txt +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/entry_points.txt +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/requires.txt +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/top_level.txt +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/pyproject.toml +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/setup.cfg +0 -0
- {mockstack-0.13.0 → mockstack-0.14.0}/uv.lock +0 -0
|
@@ -32,6 +32,17 @@ MOCKSTACK__PROXYRULES_REVERSE_PROXY_TIMEOUT = 10.0
|
|
|
32
32
|
MOCKSTACK__PROXYRULES_SIMULATE_CREATE_ON_MISSING = true
|
|
33
33
|
MOCKSTACK__PROXYRULES_VERIFY_SSL_CERTIFICATES = true
|
|
34
34
|
|
|
35
|
+
# record mode for the proxyrules strategy: off (default), missing or overwrite.
|
|
36
|
+
# missing records a fixture rule's file from the next matching URL rule when the file
|
|
37
|
+
# does not exist yet; overwrite also re-records files recorded before. Requests really
|
|
38
|
+
# reach the upstream until recorded. Never enable on a shared or exposed instance.
|
|
39
|
+
# See https://promptromp.github.io/mockstack/strategies/proxyrules/#recording-fixtures
|
|
40
|
+
MOCKSTACK__PROXYRULES_RECORD_MODE = off
|
|
41
|
+
# directory every recorded fixture must resolve inside; required when recording.
|
|
42
|
+
# MOCKSTACK__PROXYRULES_RECORD_ROOT = "/some/path/fixtures/"
|
|
43
|
+
# optional module:function called on every body before it is recorded (must be importable).
|
|
44
|
+
# MOCKSTACK__PROXYRULES_RECORD_SCRUBBER = "my_scrubbers:mask_emails"
|
|
45
|
+
|
|
35
46
|
# OpenTelemetry settings
|
|
36
47
|
MOCKSTACK__OPENTELEMETRY__ENABLED = true
|
|
37
48
|
MOCKSTACK__OPENTELEMETRY__ENDPOINT = "http://localhost:4317/"
|
|
@@ -10,9 +10,11 @@ request to one *strategy*, selected with the `strategy` setting:
|
|
|
10
10
|
- `filefixtures`: renders Jinja templates from `templates_dir`, chosen from the request
|
|
11
11
|
path, and simulates resource creation, searches and commands for POSTs.
|
|
12
12
|
- `proxyrules`: evaluates an ordered YAML rules file. The first matching rule serves a
|
|
13
|
-
`file:///` fixture
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
`file:///` fixture (with the rule's optional `status` and `response_headers`),
|
|
14
|
+
reverse-proxies to a real service, or redirects. In record mode
|
|
15
|
+
(`proxyrules_record_mode`) a missing fixture is recorded from the next matching URL
|
|
16
|
+
rule. Rules match on path, method and `headers`/`query`/`body`/`json` predicates, and
|
|
17
|
+
every response is stamped with `X-Mockstack-Result` and `X-Mockstack-Rule`.
|
|
16
18
|
|
|
17
19
|
Both share `CreateMixin` for simulated creates. Settings come from pydantic-settings:
|
|
18
20
|
CLI flags, `MOCKSTACK__*` environment variables, or a `.env` file.
|
|
@@ -25,6 +27,8 @@ CLI flags, `MOCKSTACK__*` environment variables, or a `.env` file.
|
|
|
25
27
|
`create_mixin.py`, `factory.py`
|
|
26
28
|
- `mockstack/rules.py`: the proxyrules `Rule`: predicates, load-time validation,
|
|
27
29
|
template context
|
|
30
|
+
- `mockstack/recording.py`: record-mode building blocks: fixture encoding that renders
|
|
31
|
+
back to the exact body, the recorded marker, root confinement and atomic writes
|
|
28
32
|
- `mockstack/templating.py`: Jinja environment and path-to-template-name resolution
|
|
29
33
|
- `mockstack/intent.py`, `mockstack/identifiers.py`: POST intent (search, command,
|
|
30
34
|
create) and path-identifier heuristics
|
|
@@ -32,11 +36,13 @@ CLI flags, `MOCKSTACK__*` environment variables, or a `.env` file.
|
|
|
32
36
|
- `mockstack/tests/`: unit tests; `conftest.py` holds fixtures shared by unit and live
|
|
33
37
|
tests (`make_settings`, `make_request`, `write_rules`, `write_template`, `span`), and
|
|
34
38
|
`strategies/conftest.py` adds strategy helpers (`traced_request`,
|
|
35
|
-
`proxyrules_strategy`, `apply_rule`). Build settings with
|
|
39
|
+
`proxyrules_strategy`, `apply_rule`, `upstream_send`, `span_attributes`). Build settings with
|
|
40
|
+
`make_settings`: it ignores
|
|
36
41
|
`MOCKSTACK__*` environment variables and `.env` files
|
|
37
42
|
- `mockstack/tests/live/`: live tests against real uvicorn servers on loopback sockets;
|
|
38
43
|
`conftest.py` provides the session-scoped recording echo `upstream`, the module-scoped
|
|
39
|
-
`mockstack_server`, `proxyrules_settings` and `render_rules`
|
|
44
|
+
`mockstack_server`, `proxyrules_settings` and `render_rules`; `test_record_mode_live.py`
|
|
45
|
+
records into temporary directories, never the repository
|
|
40
46
|
- `examples/`: runnable examples; `examples/proxyrules-cookbook/` holds the files
|
|
41
47
|
embedded in the cookbook docs page
|
|
42
48
|
- `docs/`: the MkDocs Material site (`mkdocs.yml`); the home page is `README.md`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: An API mocking workhorse
|
|
5
5
|
Author-email: Adam Ever-Hadani <mockstack.contact@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -63,6 +63,8 @@ Highlights include:
|
|
|
63
63
|
* Rule predicates for the `proxyrules` strategy: match requests on path, method, headers, query parameters and JSON body fields, then serve a fixture, reverse-proxy to a real service, or redirect. :dart:
|
|
64
64
|
* Dynamic replacements: a rule's replacement can be a Jinja template, so a request header can pick the fixture scenario to serve. :twisted_rightwards_arrows:
|
|
65
65
|
* Result headers: every `proxyrules` response is stamped with `X-Mockstack-Result` and `X-Mockstack-Rule`, so a test can assert it got a fixture and not the real service. :label:
|
|
66
|
+
* Fixture status codes and headers: a `proxyrules` fixture can answer with any status and extra headers, e.g. a 503 with `Retry-After`, to test how a client handles a failing dependency. :vertical_traffic_light:
|
|
67
|
+
* Record mode: `proxyrules` can record real responses into the fixtures its rules serve, with an optional scrubber, then replay them without the real service. :red_circle:
|
|
66
68
|
* Observability via [OpenTelemetry](https://opentelemetry.io/) integration. Get detailed traces of your sessions instantly reported to backends such as [Grafana](https://grafana.com/), [Jaeger](https://www.jaegertracing.io/), [Zipkin](https://zipkin.io/), etc. :eyes:
|
|
67
69
|
* Configurability via [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) supports customizing behaviour via environment variables and a `.env` file. :flags:
|
|
68
70
|
* Comprehensive unit-tests, linting and formatting coverage as well as vulnerabilities and security scanning with full CI automation to ensure stability and a high-quality codebase for production-grade use. :+1:
|
|
@@ -141,8 +143,19 @@ curl -i -H "X-Test-Run: ci-42" http://127.0.0.1:8000/projects/api/v1/project/pro
|
|
|
141
143
|
curl -i http://127.0.0.1:8000/projects/api/v1/project/proj-123
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
The [ProxyRules cookbook](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/) walks through this recipe and more (per-scenario fixtures, matching on request bodies and query parameters, asserting in tests), each backed by a live test.
|
|
146
|
+
The [ProxyRules cookbook](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/) walks through this recipe and more (per-scenario fixtures, matching on request bodies and query parameters, asserting in tests, error responses, recording fixtures), each backed by a live test.
|
|
145
147
|
|
|
148
|
+
### Record fixtures from a real service
|
|
149
|
+
|
|
150
|
+
Instead of writing fixtures by hand, record them: with record mode on, a fixture rule whose file does not exist yet sends the request on to the next matching URL rule, saves the response into the fixture file, and serves it from there. Later requests are replayed from the file without calling the service. Record mode is off by default:
|
|
151
|
+
|
|
152
|
+
```shell
|
|
153
|
+
MOCKSTACK__STRATEGY=proxyrules MOCKSTACK__PROXYRULES_RULES_FILENAME=rules.yml \
|
|
154
|
+
MOCKSTACK__PROXYRULES_RECORD_MODE=missing MOCKSTACK__PROXYRULES_RECORD_ROOT=fixtures \
|
|
155
|
+
uv run mockstack
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Requests really reach the service until their response is recorded, so record against a safe environment and review the recorded files before committing them. An optional scrubber can mask sensitive data first; see [Recording fixtures](https://promptromp.github.io/mockstack/strategies/proxyrules/#recording-fixtures) and the cookbook's [Record fixtures from a real service](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/#9-record-fixtures-from-a-real-service).
|
|
146
159
|
|
|
147
160
|
## Testing
|
|
148
161
|
|
|
@@ -29,6 +29,8 @@ Highlights include:
|
|
|
29
29
|
* Rule predicates for the `proxyrules` strategy: match requests on path, method, headers, query parameters and JSON body fields, then serve a fixture, reverse-proxy to a real service, or redirect. :dart:
|
|
30
30
|
* Dynamic replacements: a rule's replacement can be a Jinja template, so a request header can pick the fixture scenario to serve. :twisted_rightwards_arrows:
|
|
31
31
|
* Result headers: every `proxyrules` response is stamped with `X-Mockstack-Result` and `X-Mockstack-Rule`, so a test can assert it got a fixture and not the real service. :label:
|
|
32
|
+
* Fixture status codes and headers: a `proxyrules` fixture can answer with any status and extra headers, e.g. a 503 with `Retry-After`, to test how a client handles a failing dependency. :vertical_traffic_light:
|
|
33
|
+
* Record mode: `proxyrules` can record real responses into the fixtures its rules serve, with an optional scrubber, then replay them without the real service. :red_circle:
|
|
32
34
|
* Observability via [OpenTelemetry](https://opentelemetry.io/) integration. Get detailed traces of your sessions instantly reported to backends such as [Grafana](https://grafana.com/), [Jaeger](https://www.jaegertracing.io/), [Zipkin](https://zipkin.io/), etc. :eyes:
|
|
33
35
|
* Configurability via [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) supports customizing behaviour via environment variables and a `.env` file. :flags:
|
|
34
36
|
* Comprehensive unit-tests, linting and formatting coverage as well as vulnerabilities and security scanning with full CI automation to ensure stability and a high-quality codebase for production-grade use. :+1:
|
|
@@ -107,8 +109,19 @@ curl -i -H "X-Test-Run: ci-42" http://127.0.0.1:8000/projects/api/v1/project/pro
|
|
|
107
109
|
curl -i http://127.0.0.1:8000/projects/api/v1/project/proj-123
|
|
108
110
|
```
|
|
109
111
|
|
|
110
|
-
The [ProxyRules cookbook](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/) walks through this recipe and more (per-scenario fixtures, matching on request bodies and query parameters, asserting in tests), each backed by a live test.
|
|
112
|
+
The [ProxyRules cookbook](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/) walks through this recipe and more (per-scenario fixtures, matching on request bodies and query parameters, asserting in tests, error responses, recording fixtures), each backed by a live test.
|
|
111
113
|
|
|
114
|
+
### Record fixtures from a real service
|
|
115
|
+
|
|
116
|
+
Instead of writing fixtures by hand, record them: with record mode on, a fixture rule whose file does not exist yet sends the request on to the next matching URL rule, saves the response into the fixture file, and serves it from there. Later requests are replayed from the file without calling the service. Record mode is off by default:
|
|
117
|
+
|
|
118
|
+
```shell
|
|
119
|
+
MOCKSTACK__STRATEGY=proxyrules MOCKSTACK__PROXYRULES_RULES_FILENAME=rules.yml \
|
|
120
|
+
MOCKSTACK__PROXYRULES_RECORD_MODE=missing MOCKSTACK__PROXYRULES_RECORD_ROOT=fixtures \
|
|
121
|
+
uv run mockstack
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Requests really reach the service until their response is recorded, so record against a safe environment and review the recorded files before committing them. An optional scrubber can mask sensitive data first; see [Recording fixtures](https://promptromp.github.io/mockstack/strategies/proxyrules/#recording-fixtures) and the cookbook's [Record fixtures from a real service](https://promptromp.github.io/mockstack/guides/proxyrules-cookbook/#9-record-fixtures-from-a-real-service).
|
|
112
125
|
|
|
113
126
|
## Testing
|
|
114
127
|
|
|
@@ -53,6 +53,9 @@ See [ProxyRules](strategies/proxyrules.md) for how rules are written and evaluat
|
|
|
53
53
|
| `proxyrules_reverse_proxy_timeout` | `MOCKSTACK__PROXYRULES_REVERSE_PROXY_TIMEOUT` | float | `10.0` | Timeout in seconds for reverse-proxied upstream requests. An upstream that does not answer in time is answered with a 504 stamped `X-Mockstack-Result: error`. `None` (when constructing `Settings` in Python) disables the timeout |
|
|
54
54
|
| `proxyrules_simulate_create_on_missing` | `MOCKSTACK__PROXYRULES_SIMULATE_CREATE_ON_MISSING` | boolean | `false` | Whether a request that matches no rule and looks like a resource creation (e.g. a POST) gets a simulated 201 (`X-Mockstack-Result: create`) instead of a 404 (`missing`) |
|
|
55
55
|
| `proxyrules_verify_ssl_certificates` | `MOCKSTACK__PROXYRULES_VERIFY_SSL_CERTIFICATES` | boolean | `true` | Whether to verify the TLS certificates of HTTPS upstreams when reverse proxying. Disable with caution, e.g. for a trusted upstream with a self-signed certificate |
|
|
56
|
+
| `proxyrules_record_mode` | `MOCKSTACK__PROXYRULES_RECORD_MODE` | string | `off` | Applies to the `proxyrules` strategy. Record mode. `missing` writes the upstream response into a fixture rule's file when that file does not exist yet; `overwrite` also re-records files that were recorded before, never hand-written ones. Requires `proxyrules_record_root` and `reverse_proxy`. Never enable on a shared or exposed instance; see [Recording fixtures](strategies/proxyrules.md#recording-fixtures). Environment variables take the lower-case values; `mockstack --help` lists the enum names (`OFF`, `MISSING`, `OVERWRITE`), and the command line accepts either form |
|
|
57
|
+
| `proxyrules_record_root` | `MOCKSTACK__PROXYRULES_RECORD_ROOT` | path | - | Applies to the `proxyrules` strategy. Existing directory that every recorded fixture file must resolve inside, symlinks followed. Required when `proxyrules_record_mode` is not `off` |
|
|
58
|
+
| `proxyrules_record_scrubber` | `MOCKSTACK__PROXYRULES_RECORD_SCRUBBER` | string | - | Optional `module:function` called with every body before it is recorded; it returns the text to write, or `None` to skip that response. Imported and checked when settings load, so the module must be importable (e.g. `PYTHONPATH=.`); see [Scrubbing recorded bodies](strategies/proxyrules.md#scrubbing-recorded-bodies) |
|
|
56
59
|
|
|
57
60
|
## Resource Creation Settings
|
|
58
61
|
|
|
@@ -117,6 +120,16 @@ MOCKSTACK__PROXYRULES_REDIRECT_VIA=reverse_proxy
|
|
|
117
120
|
MOCKSTACK__PROXYRULES_REVERSE_PROXY_TIMEOUT=5
|
|
118
121
|
```
|
|
119
122
|
|
|
123
|
+
And one that records fixtures from the real services behind the rules (see
|
|
124
|
+
[Recording fixtures](strategies/proxyrules.md#recording-fixtures)):
|
|
125
|
+
|
|
126
|
+
```env
|
|
127
|
+
MOCKSTACK__STRATEGY=proxyrules
|
|
128
|
+
MOCKSTACK__PROXYRULES_RULES_FILENAME=./rules.yml
|
|
129
|
+
MOCKSTACK__PROXYRULES_RECORD_MODE=missing
|
|
130
|
+
MOCKSTACK__PROXYRULES_RECORD_ROOT=/path/to/fixtures
|
|
131
|
+
```
|
|
132
|
+
|
|
120
133
|
## Command Line Usage
|
|
121
134
|
|
|
122
135
|
You can also set configuration options via command line arguments:
|
|
@@ -124,4 +137,5 @@ You can also set configuration options via command line arguments:
|
|
|
124
137
|
```bash
|
|
125
138
|
uvx mockstack --strategy filefixtures --templates-dir ~/mockstack-templates/
|
|
126
139
|
uvx mockstack --strategy proxyrules --proxyrules-rules-filename ./rules.yml --proxyrules-redirect-via http_307_temporary
|
|
140
|
+
uvx mockstack --strategy proxyrules --proxyrules-rules-filename ./rules.yml --proxyrules-record-mode missing --proxyrules-record-root ./fixtures
|
|
127
141
|
```
|
|
@@ -416,3 +416,127 @@ Things to know before choosing redirect mode:
|
|
|
416
416
|
- The client must be able to reach the upstream itself, and must follow redirects.
|
|
417
417
|
- The original query string is kept: it is appended to the rewritten URL in
|
|
418
418
|
`location` (with `&` if the replacement already has a query of its own).
|
|
419
|
+
|
|
420
|
+
## 8. Fixture status codes and headers
|
|
421
|
+
|
|
422
|
+
A fixture is answered with HTTP 200 unless its rule sets `status`, which makes it easy to
|
|
423
|
+
test how a client handles a dependency that is down, throttled, or answers with a
|
|
424
|
+
non-200 success. `response_headers` adds headers such as `Retry-After` or `Location`; a
|
|
425
|
+
list value sends a header once per item, and a `Content-Type` replaces the type inferred
|
|
426
|
+
from the file suffix. The response is still stamped `X-Mockstack-Result: template`: the
|
|
427
|
+
status came from your fixture, so `error` keeps meaning that mockstack itself failed.
|
|
428
|
+
|
|
429
|
+
```yaml title="08-status-and-headers/rules.yml"
|
|
430
|
+
--8<-- "examples/proxyrules-cookbook/08-status-and-headers/rules.yml"
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```jinja title="08-status-and-headers/fixtures/errors/unavailable.json.j2"
|
|
434
|
+
--8<-- "examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/unavailable.json.j2"
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```jinja title="08-status-and-headers/fixtures/errors/rate-limited.json.j2"
|
|
438
|
+
--8<-- "examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/rate-limited.json.j2"
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```jinja title="08-status-and-headers/fixtures/orders/created.json.j2"
|
|
442
|
+
--8<-- "examples/proxyrules-cookbook/08-status-and-headers/fixtures/orders/created.json.j2"
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
curl -i -H "X-Test-Scenario: outage" http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
|
|
447
|
+
# HTTP/1.1 503 Service Unavailable
|
|
448
|
+
# retry-after: 30
|
|
449
|
+
# x-mockstack-result: template
|
|
450
|
+
# x-mockstack-rule: orders-outage
|
|
451
|
+
# {"error": "service unavailable", "order_id": "ord-1001"}
|
|
452
|
+
|
|
453
|
+
curl -i -H "X-Test-Scenario: throttled" http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
|
|
454
|
+
# HTTP/1.1 429 Too Many Requests
|
|
455
|
+
# content-type: application/problem+json
|
|
456
|
+
# retry-after: 5
|
|
457
|
+
# x-mockstack-result: template
|
|
458
|
+
# x-mockstack-rule: orders-rate-limited
|
|
459
|
+
# {"type": "about:blank", "title": "Too Many Requests", "status": 429}
|
|
460
|
+
|
|
461
|
+
curl -i -H "Content-Type: application/json" -d '{"customer": "cust-7"}' http://127.0.0.1:8000/orders/api/v1/orders
|
|
462
|
+
# HTTP/1.1 201 Created
|
|
463
|
+
# location: /orders/api/v1/orders/ord-1001
|
|
464
|
+
# x-mockstack-result: template
|
|
465
|
+
# x-mockstack-rule: order-created
|
|
466
|
+
# {"id": "ord-1001", "status": "OPEN", "customer": "cust-7"}
|
|
467
|
+
|
|
468
|
+
curl -i http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
|
|
469
|
+
# HTTP/1.1 200 OK
|
|
470
|
+
# x-mockstack-result: proxy
|
|
471
|
+
# x-mockstack-rule: orders-passthrough
|
|
472
|
+
# {"source":"upstream","path":"/api/v1/orders/ord-1001","method":"GET",...}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Things to know:
|
|
476
|
+
|
|
477
|
+
- The status and headers apply only once the fixture has rendered. A missing fixture is
|
|
478
|
+
still a 404 stamped `error`, without them.
|
|
479
|
+
- A `204` or `304` is sent without a body, but its fixture file must still exist.
|
|
480
|
+
- `status` and `response_headers` only work on `file:///` fixture rules: mockstack
|
|
481
|
+
refuses to start when a rule whose `replacement` is a URL sets them.
|
|
482
|
+
- Headers that mockstack manages cannot be set: `Content-Length`, hop-by-hop headers
|
|
483
|
+
such as `Connection` and `Transfer-Encoding`, `Date`, `Server` and the
|
|
484
|
+
`X-Mockstack-*` result headers.
|
|
485
|
+
|
|
486
|
+
## 9. Record fixtures from a real service
|
|
487
|
+
|
|
488
|
+
Instead of writing fixtures by hand, let mockstack record them. In record mode, a fixture
|
|
489
|
+
rule whose file does not exist yet sends the request on to the next matching URL rule,
|
|
490
|
+
writes the response into the fixture file, and answers from that file. A scrubber can
|
|
491
|
+
rewrite each body before it is written, here to mask e-mail addresses. Start mockstack
|
|
492
|
+
from the recipe directory:
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
mkdir -p fixtures
|
|
496
|
+
PYTHONPATH=. MOCKSTACK__STRATEGY=proxyrules MOCKSTACK__PROXYRULES_RULES_FILENAME=rules.local.yml \
|
|
497
|
+
MOCKSTACK__PROXYRULES_RECORD_MODE=missing MOCKSTACK__PROXYRULES_RECORD_ROOT=fixtures \
|
|
498
|
+
MOCKSTACK__PROXYRULES_RECORD_SCRUBBER=scrubbers:mask_emails uv run mockstack
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
```yaml title="09-recording/rules.yml"
|
|
502
|
+
--8<-- "examples/proxyrules-cookbook/09-recording/rules.yml"
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
<!-- fmt: off -->
|
|
506
|
+
```python title="09-recording/scrubbers.py"
|
|
507
|
+
--8<-- "examples/proxyrules-cookbook/09-recording/scrubbers.py"
|
|
508
|
+
```
|
|
509
|
+
<!-- fmt: on -->
|
|
510
|
+
|
|
511
|
+
The first request is recorded from the echo upstream, with the address masked:
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
curl -i "http://127.0.0.1:8000/users/api/v1/users/user-7?contact=ada@example.com"
|
|
515
|
+
# HTTP/1.1 200 OK
|
|
516
|
+
# x-mockstack-result: record
|
|
517
|
+
# x-mockstack-rule: users-recorded
|
|
518
|
+
# {"source":"upstream","path":"/api/v1/users/user-7","method":"GET","query":{"contact":"***@***"},...}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`fixtures/users/user-7.json.j2` now holds that body, after a `{# mockstack:recorded #}`
|
|
522
|
+
marker. The same request is then served from the file, without calling the upstream:
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
curl -i "http://127.0.0.1:8000/users/api/v1/users/user-7?contact=ada@example.com"
|
|
526
|
+
# HTTP/1.1 200 OK
|
|
527
|
+
# x-mockstack-result: template
|
|
528
|
+
# x-mockstack-rule: users-recorded
|
|
529
|
+
# {"source":"upstream","path":"/api/v1/users/user-7","method":"GET","query":{"contact":"***@***"},...}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Things to know:
|
|
533
|
+
|
|
534
|
+
- Until a fixture is recorded, its requests really go to the real service, including
|
|
535
|
+
`POST`, `PUT` and `DELETE` -- so record against a safe environment, never production.
|
|
536
|
+
- Restart without the three `RECORD` settings to replay only. `overwrite` mode re-records
|
|
537
|
+
files carrying the marker and never touches hand-written fixtures.
|
|
538
|
+
- A response is recorded only when its status matches the rule's `status` (200 by
|
|
539
|
+
default) and its body is text; otherwise it is returned stamped `proxy`. See
|
|
540
|
+
[Recording fixtures](../strategies/proxyrules.md#recording-fixtures).
|
|
541
|
+
- Recorded files contain whatever the real service returned. Review them before
|
|
542
|
+
committing, and never run record mode on a shared or exposed instance.
|
|
@@ -16,7 +16,7 @@ This strategy:
|
|
|
16
16
|
- Matches requests on a path regex, the HTTP method, and optional `headers`, `query`,
|
|
17
17
|
`body` and `json` predicates
|
|
18
18
|
- Serves `file:///` Jinja templates as fixtures, with request data in the template
|
|
19
|
-
context
|
|
19
|
+
context, and the rule's own `status` and `response_headers`
|
|
20
20
|
- Renders a `replacement` that contains Jinja delimiters as a template of its own
|
|
21
21
|
(dynamic replacements), e.g. to pick a fixture directory from a header
|
|
22
22
|
- Reverse-proxies to the rewritten URL by default, or answers with an HTTP redirect
|
|
@@ -24,6 +24,7 @@ This strategy:
|
|
|
24
24
|
`X-Mockstack-Rule` headers
|
|
25
25
|
- Validates and compiles every rule at startup, so a broken rules file fails fast
|
|
26
26
|
- Can simulate resource creation for requests that match no rule
|
|
27
|
+
- Can record real upstream responses into the fixture files its rules serve (record mode)
|
|
27
28
|
- Provides OpenTelemetry integration for observability
|
|
28
29
|
|
|
29
30
|
## Configuration
|
|
@@ -86,6 +87,11 @@ rules:
|
|
|
86
87
|
non-ASCII characters as they are. A path that is absent never matches, while a
|
|
87
88
|
present `null` matches as the text `null`. A rule with `body`/`json` never matches a
|
|
88
89
|
request without a body.
|
|
90
|
+
- `status`: Optional HTTP status for the response of a [file template](#file-templates)
|
|
91
|
+
rule, an integer from 200 to 599 (default 200). See
|
|
92
|
+
[Status codes and headers](#status-codes-and-headers).
|
|
93
|
+
- `response_headers`: Optional mapping of header name -> value added to the response of
|
|
94
|
+
a file template rule. A list value sends the header once per item.
|
|
89
95
|
|
|
90
96
|
Predicate values must be strings. YAML reads unquoted `2`, `true` or `2024` as
|
|
91
97
|
numbers and booleans; mockstack converts them back with `str()`, which turns `true`
|
|
@@ -145,6 +151,12 @@ offending rule, when:
|
|
|
145
151
|
- a Jinja `replacement` has a syntax error;
|
|
146
152
|
- a `replacement` mixes Jinja delimiters with a regex backreference (`\1`, `\g<id>`)
|
|
147
153
|
in its literal text;
|
|
154
|
+
- `status` is not an integer from 200 to 599;
|
|
155
|
+
- a `response_headers` entry has an invalid name, no value, or a value that is not a
|
|
156
|
+
string or number, contains a control character, starts or ends with whitespace or is
|
|
157
|
+
not Latin-1, or names a header mockstack manages;
|
|
158
|
+
- `status` or `response_headers` is set on a rule whose `replacement` is a plain URL
|
|
159
|
+
rather than a `file:///` fixture;
|
|
148
160
|
- a named group in `pattern` shadows a reserved template variable (`path`, `method`,
|
|
149
161
|
`query`, `headers`, `request_json`, `groups`).
|
|
150
162
|
|
|
@@ -194,10 +206,51 @@ rules:
|
|
|
194
206
|
```
|
|
195
207
|
|
|
196
208
|
The response content type comes from the file suffix, ignoring a trailing `.j2`
|
|
197
|
-
(`project.json.j2` -> `application/json`). A successfully rendered template
|
|
198
|
-
|
|
209
|
+
(`project.json.j2` -> `application/json`). A successfully rendered template returns
|
|
210
|
+
HTTP 200, or the rule's `status`, stamped `X-Mockstack-Result: template`; see
|
|
199
211
|
[Error handling](#error-handling) for the failure cases. Use the `tojson` filter to
|
|
200
|
-
write request values into JSON fixtures, e.g. `{"id": {{ id | tojson }}}`.
|
|
212
|
+
write request values into JSON fixtures, e.g. `{"id": {{ id | tojson }}}`. Fixture files
|
|
213
|
+
and the rules file are read as UTF-8, matching how filefixtures reads its templates.
|
|
214
|
+
|
|
215
|
+
### Status codes and headers
|
|
216
|
+
|
|
217
|
+
`status` and `response_headers` set the status and extra headers a fixture is served
|
|
218
|
+
with, for example to simulate a dependency that is down:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
rules:
|
|
222
|
+
- name: orders-outage
|
|
223
|
+
method: GET
|
|
224
|
+
pattern: ^/orders/api/v1/orders/(?P<order_id>[a-z0-9-]+)$
|
|
225
|
+
headers:
|
|
226
|
+
x-test-scenario: outage
|
|
227
|
+
status: 503
|
|
228
|
+
response_headers:
|
|
229
|
+
Retry-After: "30"
|
|
230
|
+
replacement: file:///fixtures/errors/unavailable.json.j2
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- The response is still stamped `X-Mockstack-Result: template`, so a test can tell a
|
|
234
|
+
fixture's 503 from a 5xx `error` that mockstack itself returned.
|
|
235
|
+
- The status and headers apply only once the fixture has rendered. A missing fixture
|
|
236
|
+
or a render failure is answered as described in [Error handling](#error-handling),
|
|
237
|
+
without them.
|
|
238
|
+
- A `204` or `304` is sent without a body; the fixture file must still exist.
|
|
239
|
+
- Header values are sent as written. A list value sends the header once per item (for
|
|
240
|
+
example several `Set-Cookie` headers), and a `Content-Type` replaces the type
|
|
241
|
+
inferred from the file suffix. As with predicates, quote values that YAML would read
|
|
242
|
+
as numbers or booleans.
|
|
243
|
+
- Headers that mockstack manages cannot be set: `Content-Length`, the hop-by-hop
|
|
244
|
+
headers (`Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`,
|
|
245
|
+
`TE`, `Trailer`, `Transfer-Encoding`, `Upgrade`), `Date`, `Server`, and the
|
|
246
|
+
`X-Mockstack-Result` and `X-Mockstack-Rule` result headers.
|
|
247
|
+
- Only fixture rules can set them. A rule whose plain `replacement` is a URL is
|
|
248
|
+
rejected at startup; a [dynamic replacement](#dynamic-replacements) is only known per
|
|
249
|
+
request, so one that renders a URL is answered with a 500 stamped `error`.
|
|
250
|
+
|
|
251
|
+
The cookbook recipe
|
|
252
|
+
[Fixture status codes and headers](../guides/proxyrules-cookbook.md#8-fixture-status-codes-and-headers)
|
|
253
|
+
shows a 503, a 429 with `application/problem+json`, and a 201 with `Location`.
|
|
201
254
|
|
|
202
255
|
### Template context
|
|
203
256
|
|
|
@@ -268,6 +321,125 @@ optional values an explicit fallback, e.g.
|
|
|
268
321
|
since no `..` segment is ever involved. Restrict predicates feeding a rendered
|
|
269
322
|
path or URL to the character classes you actually expect, e.g. `[a-z0-9_-]+`.
|
|
270
323
|
|
|
324
|
+
## Recording fixtures
|
|
325
|
+
|
|
326
|
+
Record mode fills in fixtures from the real service. When a fixture rule matches and its
|
|
327
|
+
file does not exist yet, mockstack sends the request to the next matching rule whose
|
|
328
|
+
`replacement` is a URL, writes the response body to the fixture file, and answers by
|
|
329
|
+
rendering that file, stamped `X-Mockstack-Result: record`. Later requests are served from
|
|
330
|
+
the file, stamped `template`, without calling the service. Existing rules files work
|
|
331
|
+
unchanged: the passthrough rule that follows a fixture rule is where it records from.
|
|
332
|
+
Later rules whose `replacement` is a plain `file:///` path are skipped when looking for it.
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
rules:
|
|
336
|
+
- name: users-fixture
|
|
337
|
+
method: GET
|
|
338
|
+
pattern: ^/users/api/v1/users/(?P<user_id>[a-z0-9-]+)$
|
|
339
|
+
headers:
|
|
340
|
+
x-test-scenario: "[a-z0-9_-]+"
|
|
341
|
+
replacement: file:///srv/fixtures/{{ headers['x-test-scenario'] }}/users/{{ user_id }}.json.j2
|
|
342
|
+
|
|
343
|
+
- name: users-passthrough
|
|
344
|
+
pattern: ^/users/(.*)
|
|
345
|
+
replacement: https://users.example/\1
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Start mockstack with `MOCKSTACK__PROXYRULES_RECORD_MODE=missing` and
|
|
349
|
+
`MOCKSTACK__PROXYRULES_RECORD_ROOT=/srv/fixtures`, run the traffic you want fixtures for,
|
|
350
|
+
then review and commit the recorded files and run without record mode.
|
|
351
|
+
|
|
352
|
+
| `proxyrules_record_mode` | Behaviour |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| `off` (default) | Fixtures are only served |
|
|
355
|
+
| `missing` | A fixture file that does not exist yet is recorded |
|
|
356
|
+
| `overwrite` | Files recorded before are recorded again, e.g. to refresh stale fixtures; hand-written fixtures are never overwritten |
|
|
357
|
+
|
|
358
|
+
A response is recorded only when:
|
|
359
|
+
|
|
360
|
+
- a later matching rule has a URL `replacement`;
|
|
361
|
+
- the request is not a `HEAD` or `OPTIONS`;
|
|
362
|
+
- the upstream's status equals the fixture rule's `status` (200 by default), so the
|
|
363
|
+
fixture replays with the status it was recorded with;
|
|
364
|
+
- the body is UTF-8 text;
|
|
365
|
+
- the body is not still compressed with a coding mockstack does not decode (for example
|
|
366
|
+
`br` without the optional `brotli` package); `gzip` and `deflate` bodies are recorded
|
|
367
|
+
decoded.
|
|
368
|
+
|
|
369
|
+
Otherwise the upstream's response is returned stamped `proxy` with the URL rule's name,
|
|
370
|
+
and the reason is logged -- at INFO for a `HEAD`/`OPTIONS` request or a scrubber that
|
|
371
|
+
returns `None`, since both are expected; every other reason is logged at WARNING. With
|
|
372
|
+
no later URL rule, a missing fixture is
|
|
373
|
+
the usual 404 `error`. An upstream that fails is the usual 502 or 504 `error`, and a
|
|
374
|
+
fixture file that cannot be written is a 500 `error`; nothing is written in either case.
|
|
375
|
+
|
|
376
|
+
A later rule's `replacement` is still rendered even when it is a
|
|
377
|
+
[dynamic replacement](#dynamic-replacements), to learn whether it gives a URL at all --
|
|
378
|
+
so a later template replacement that fails (for example one that references a missing
|
|
379
|
+
header) turns that record attempt into a 500, the same as it would for a plain proxied
|
|
380
|
+
request to that rule.
|
|
381
|
+
|
|
382
|
+
Recorded files:
|
|
383
|
+
|
|
384
|
+
- start with `{# mockstack:recorded #}`, which renders to nothing and is how `overwrite`
|
|
385
|
+
tells them from hand-written fixtures;
|
|
386
|
+
- contain the body exactly: any Jinja syntax or carriage return in it is printed by an
|
|
387
|
+
expression, so it is never evaluated and replays byte for byte;
|
|
388
|
+
- are written to a temporary file and renamed into place, so concurrent requests never
|
|
389
|
+
read a partial fixture;
|
|
390
|
+
- must resolve, symlinks followed, inside `proxyrules_record_root`. A fixture path
|
|
391
|
+
outside it is served as usual and not recorded, with a warning the first time for each
|
|
392
|
+
rule.
|
|
393
|
+
|
|
394
|
+
While a fixture has not been recorded yet:
|
|
395
|
+
|
|
396
|
+
- requests really reach the upstream, including non-idempotent methods such as `POST`,
|
|
397
|
+
`PUT` and `DELETE` -- recording a create endpoint creates real resources there;
|
|
398
|
+
- a response that cannot be recorded (a status mismatch, a still-encoded or non-UTF-8
|
|
399
|
+
body, or a scrubber that returns `None`) is sent to the upstream again on every
|
|
400
|
+
request, not only the first;
|
|
401
|
+
- there is no locking between requests: concurrent first requests for the same fixture
|
|
402
|
+
each reach the upstream, and the last write to complete wins. A reader never sees a
|
|
403
|
+
partial file, but overlapping requests can each record a different upstream response;
|
|
404
|
+
- `HEAD` and `OPTIONS` requests for a fixture that may still be recorded are proxied
|
|
405
|
+
rather than served from a fixture file; in `overwrite` mode this also applies to a
|
|
406
|
+
fixture that was already recorded.
|
|
407
|
+
|
|
408
|
+
Only the response body is recorded. The rule's `status` and `response_headers` apply when
|
|
409
|
+
it is replayed, and the content type follows the file suffix as for any fixture.
|
|
410
|
+
|
|
411
|
+
### Scrubbing recorded bodies
|
|
412
|
+
|
|
413
|
+
`proxyrules_record_scrubber` names a `module:function` that is called with every body
|
|
414
|
+
before it is written. It returns the text to write, or `None` to skip recording that
|
|
415
|
+
response, which is then returned stamped `proxy`:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
def mask_emails(body: str, *, request: Request, rule_name: str | None, path: Path) -> str | None:
|
|
419
|
+
return EMAIL.sub("***@***", body)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The reference is imported and checked when the settings are loaded, so a typo or a
|
|
423
|
+
target that is not callable stops mockstack from starting; the module must be
|
|
424
|
+
importable, e.g. with `PYTHONPATH=.`. A scrubber that raises, or returns something
|
|
425
|
+
other than a string or `None`, answers that request with a 500 `error` and nothing is
|
|
426
|
+
written. `rule_name` is the matched fixture rule's `name` (`None` when the rule has
|
|
427
|
+
none), and `path` is the resolved fixture file path that will be written.
|
|
428
|
+
|
|
429
|
+
The scrubber must be a regular synchronous function, not `async def`: it is called
|
|
430
|
+
directly, not awaited, so an `async def` scrubber returns a coroutine object rather
|
|
431
|
+
than a string, and the request gets a 500 the same as any other non-`str`, non-`None`
|
|
432
|
+
return value. See the cookbook's
|
|
433
|
+
[Record fixtures from a real service](../guides/proxyrules-cookbook.md#9-record-fixtures-from-a-real-service).
|
|
434
|
+
|
|
435
|
+
!!! warning
|
|
436
|
+
Record mode writes files whose paths can be chosen by request data and whose content
|
|
437
|
+
comes from the upstream. Enable it only for a recording session on a trusted
|
|
438
|
+
network, never on a shared or exposed instance, and review recorded fixtures before
|
|
439
|
+
committing them: they contain whatever the real service returned. A test harness
|
|
440
|
+
that asserts `X-Mockstack-Result: template` fails on `record`, so a gating run that
|
|
441
|
+
accidentally records is caught.
|
|
442
|
+
|
|
271
443
|
## Redirection methods
|
|
272
444
|
|
|
273
445
|
`proxyrules_redirect_via` selects what a rule whose `replacement` is a URL does.
|
|
@@ -326,12 +498,13 @@ responses -- carries:
|
|
|
326
498
|
|
|
327
499
|
| Header | Value |
|
|
328
500
|
| --- | --- |
|
|
329
|
-
| `X-Mockstack-Result` | `template`, `proxy`, `redirect`, `create`, `missing` or `error` |
|
|
501
|
+
| `X-Mockstack-Result` | `template`, `proxy`, `redirect`, `create`, `missing`, `record` or `error` |
|
|
330
502
|
| `X-Mockstack-Rule` | The matched rule's `name` (or its `pattern` when unnamed); absent when no rule matched |
|
|
331
503
|
|
|
332
504
|
| `X-Mockstack-Result` | Status | Meaning |
|
|
333
505
|
| --- | --- | --- |
|
|
334
|
-
| `template` | 200 | A `file:///` fixture was rendered |
|
|
506
|
+
| `template` | 200, or the rule's `status` | A `file:///` fixture was rendered |
|
|
507
|
+
| `record` | The rule's `status` (200 by default) | Record mode wrote the upstream response to the fixture file and served it from that file |
|
|
335
508
|
| `proxy` | Upstream's | The request was reverse-proxied to the rewritten URL |
|
|
336
509
|
| `redirect` | 301 / 307 | An HTTP redirect to the rewritten URL |
|
|
337
510
|
| `create` | 201 | No rule matched; resource creation was simulated |
|
|
@@ -409,7 +582,11 @@ The default metadata fields are controlled via the configuration file and at the
|
|
|
409
582
|
## OpenTelemetry integration
|
|
410
583
|
|
|
411
584
|
The strategy automatically adds the following OpenTelemetry attributes when a rule
|
|
412
|
-
matches
|
|
585
|
+
matches. The shared rule attributes (`rule_name`, `rule_method`, `rule_pattern`,
|
|
586
|
+
`rule_replacement`) always describe the rule named in the response's
|
|
587
|
+
`X-Mockstack-Rule` header, never some other rule that was merely consulted along the
|
|
588
|
+
way (e.g. a record-mode upstream rule whose response ends up recorded and replayed
|
|
589
|
+
from the fixture instead):
|
|
413
590
|
|
|
414
591
|
- `mockstack.proxyrules.rule_name`: The name of the matched rule (if specified)
|
|
415
592
|
- `mockstack.proxyrules.rule_method`: The HTTP method the rule matches (if specified)
|
|
@@ -417,8 +594,31 @@ matches:
|
|
|
417
594
|
- `mockstack.proxyrules.rule_replacement`: The replacement as written in the rules file
|
|
418
595
|
- `mockstack.proxyrules.rewritten_url`: The final URL after applying the rule (proxy
|
|
419
596
|
and redirect results)
|
|
420
|
-
- `mockstack.proxyrules.
|
|
421
|
-
`
|
|
597
|
+
- `mockstack.proxyrules.result_type`: The result type stamped on the response (`proxy`,
|
|
598
|
+
`redirect`, `template`, `error`, or in record mode `record`), set on every proxy,
|
|
599
|
+
redirect, template and error result
|
|
600
|
+
- `mockstack.proxyrules.template_path`: The rendered fixture path (template results)
|
|
601
|
+
|
|
602
|
+
An error response that names a rule (`X-Mockstack-Result: error` with an
|
|
603
|
+
`X-Mockstack-Rule` header -- e.g. a missing or unrenderable fixture, an unreachable
|
|
604
|
+
upstream, or a record-mode write failure) carries that rule's shared attributes and
|
|
605
|
+
`result_type` `error`, overwriting any result type an earlier, now-superseded call had
|
|
606
|
+
set for the same response (e.g. a reverse proxy attempt that set `proxy` before it
|
|
607
|
+
failed).
|
|
608
|
+
|
|
609
|
+
In record mode (`proxyrules_record_mode`), attempting to record from the next matching
|
|
610
|
+
URL rule also sets:
|
|
611
|
+
|
|
612
|
+
- `mockstack.proxyrules.upstream_rule_name`: The name (or pattern, when unnamed) of the
|
|
613
|
+
rule the request was proxied to while attempting to record
|
|
614
|
+
- `mockstack.proxyrules.rewritten_url`: The upstream URL the request was proxied to while
|
|
615
|
+
attempting to record, whether or not the response ended up recorded
|
|
616
|
+
- `mockstack.proxyrules.recorded_path`: The fixture path written, when the response was
|
|
617
|
+
recorded (`result_type` is `record`; the shared rule attributes describe the fixture
|
|
618
|
+
rule)
|
|
619
|
+
- `mockstack.proxyrules.not_recorded_reason`: Why the response was not recorded, when it
|
|
620
|
+
was not (`result_type` is `proxy`; the shared rule attributes describe the upstream
|
|
621
|
+
rule, since that is what `X-Mockstack-Rule` names)
|
|
422
622
|
|
|
423
623
|
## Testing evaluation suites
|
|
424
624
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"error": "service unavailable", "order_id": {{ order_id | tojson }}}
|
mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/orders/created.json.j2
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id": "ord-1001", "status": "OPEN", "customer": {{ request_json.customer | tojson }}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
# 503 with Retry-After: the orders service is down for this scenario.
|
|
3
|
+
- name: orders-outage
|
|
4
|
+
method: GET
|
|
5
|
+
pattern: ^/orders/api/v1/orders/(?P<order_id>[a-z0-9-]+)$
|
|
6
|
+
headers:
|
|
7
|
+
x-test-scenario: outage
|
|
8
|
+
status: 503
|
|
9
|
+
response_headers:
|
|
10
|
+
Retry-After: "30"
|
|
11
|
+
replacement: file://${FIXTURES_DIR}/errors/unavailable.json.j2
|
|
12
|
+
|
|
13
|
+
# 429 with a problem+json body, which replaces the type inferred from .json.j2.
|
|
14
|
+
- name: orders-rate-limited
|
|
15
|
+
method: GET
|
|
16
|
+
pattern: ^/orders/api/v1/orders/(?P<order_id>[a-z0-9-]+)$
|
|
17
|
+
headers:
|
|
18
|
+
x-test-scenario: throttled
|
|
19
|
+
status: 429
|
|
20
|
+
response_headers:
|
|
21
|
+
Content-Type: application/problem+json
|
|
22
|
+
Retry-After: "5"
|
|
23
|
+
replacement: file://${FIXTURES_DIR}/errors/rate-limited.json.j2
|
|
24
|
+
|
|
25
|
+
# 201 Created, with a Location header for the new order.
|
|
26
|
+
- name: order-created
|
|
27
|
+
method: POST
|
|
28
|
+
pattern: ^/orders/api/v1/orders$
|
|
29
|
+
status: 201
|
|
30
|
+
response_headers:
|
|
31
|
+
Location: /orders/api/v1/orders/ord-1001
|
|
32
|
+
replacement: file://${FIXTURES_DIR}/orders/created.json.j2
|
|
33
|
+
|
|
34
|
+
- name: orders-passthrough
|
|
35
|
+
pattern: ^/orders/(.*)
|
|
36
|
+
replacement: ${UPSTREAM_URL}/\1
|