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.
Files changed (150) hide show
  1. {mockstack-0.13.0 → mockstack-0.14.0}/.env.example +11 -0
  2. {mockstack-0.13.0 → mockstack-0.14.0}/.gitignore +1 -0
  3. {mockstack-0.13.0 → mockstack-0.14.0}/CLAUDE.md +11 -5
  4. {mockstack-0.13.0 → mockstack-0.14.0}/Dockerfile +1 -1
  5. {mockstack-0.13.0 → mockstack-0.14.0}/PKG-INFO +15 -2
  6. {mockstack-0.13.0 → mockstack-0.14.0}/README.md +14 -1
  7. {mockstack-0.13.0 → mockstack-0.14.0}/docs/configuration.md +14 -0
  8. {mockstack-0.13.0 → mockstack-0.14.0}/docs/guides/proxyrules-cookbook.md +124 -0
  9. {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/proxyrules.md +209 -9
  10. mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/rate-limited.json.j2 +1 -0
  11. mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/errors/unavailable.json.j2 +1 -0
  12. mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/fixtures/orders/created.json.j2 +1 -0
  13. mockstack-0.14.0/examples/proxyrules-cookbook/08-status-and-headers/rules.yml +36 -0
  14. mockstack-0.14.0/examples/proxyrules-cookbook/09-recording/rules.yml +11 -0
  15. mockstack-0.14.0/examples/proxyrules-cookbook/09-recording/scrubbers.py +14 -0
  16. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/README.md +7 -1
  17. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/.env.example +5 -0
  18. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/config.py +24 -1
  19. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/constants.py +26 -0
  20. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/display.py +16 -1
  21. mockstack-0.14.0/mockstack/recording.py +77 -0
  22. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/rules.py +93 -4
  23. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/filefixtures.py +1 -2
  24. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/proxyrules.py +281 -39
  25. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/conftest.py +11 -3
  26. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_cookbook.py +110 -1
  27. mockstack-0.14.0/mockstack/tests/live/test_fixture_status_live.py +73 -0
  28. mockstack-0.14.0/mockstack/tests/live/test_record_mode_live.py +142 -0
  29. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/conftest.py +22 -0
  30. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_proxyrules.py +326 -9
  31. mockstack-0.14.0/mockstack/tests/strategies/test_proxyrules_record.py +552 -0
  32. mockstack-0.14.0/mockstack/tests/test_config.py +168 -0
  33. mockstack-0.14.0/mockstack/tests/test_display.py +51 -0
  34. mockstack-0.14.0/mockstack/tests/test_recording.py +176 -0
  35. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_rules.py +143 -0
  36. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/PKG-INFO +15 -2
  37. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/SOURCES.txt +12 -1
  38. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/scm_file_list.json +11 -0
  39. mockstack-0.14.0/mockstack.egg-info/scm_version.json +8 -0
  40. mockstack-0.13.0/mockstack/tests/test_config.py +0 -58
  41. mockstack-0.13.0/mockstack/tests/test_display.py +0 -20
  42. mockstack-0.13.0/mockstack.egg-info/scm_version.json +0 -8
  43. {mockstack-0.13.0 → mockstack-0.14.0}/.dockerignore +0 -0
  44. {mockstack-0.13.0 → mockstack-0.14.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  45. {mockstack-0.13.0 → mockstack-0.14.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  46. {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/ci.yml +0 -0
  47. {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/publish-docs.yml +0 -0
  48. {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/publish-to-pypi.yml +0 -0
  49. {mockstack-0.13.0 → mockstack-0.14.0}/.github/workflows/smoke-test-pypi.yml +0 -0
  50. {mockstack-0.13.0 → mockstack-0.14.0}/.pre-commit-config.yaml +0 -0
  51. {mockstack-0.13.0 → mockstack-0.14.0}/CODE_OF_CONDUCT.md +0 -0
  52. {mockstack-0.13.0 → mockstack-0.14.0}/CONTRIBUTING.md +0 -0
  53. {mockstack-0.13.0 → mockstack-0.14.0}/LICENSE +0 -0
  54. {mockstack-0.13.0 → mockstack-0.14.0}/SECURITY.md +0 -0
  55. {mockstack-0.13.0 → mockstack-0.14.0}/docker-entrypoint.sh +0 -0
  56. {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/favicon.ico +0 -0
  57. {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/logo.png +0 -0
  58. {mockstack-0.13.0 → mockstack-0.14.0}/docs/assets/mockstack.png +0 -0
  59. {mockstack-0.13.0 → mockstack-0.14.0}/docs/ollama.md +0 -0
  60. {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/base.md +0 -0
  61. {mockstack-0.13.0 → mockstack-0.14.0}/docs/strategies/filefixtures.md +0 -0
  62. {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/.env.example +0 -0
  63. {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/README.md +0 -0
  64. {mockstack-0.13.0 → mockstack-0.14.0}/examples/filefixtures-with-templates/templates/servicename-api-v1-items.j2 +0 -0
  65. {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
  66. {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/README.md +0 -0
  67. {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/mockstack-langchain-example.ipynb +0 -0
  68. {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/ollama-openai-v1-chat-completions.j2 +0 -0
  69. {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/openai-v1-chat-completions.j2 +0 -0
  70. {mockstack-0.13.0 → mockstack-0.14.0}/examples/llm/templates/openai-v1-deployments-gpt-4o-chat-completions.j2 +0 -0
  71. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/01-tagged-traffic/fixtures/projects/project.json.j2 +0 -0
  72. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/01-tagged-traffic/rules.yml +0 -0
  73. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/fixtures/archived/projects/project.json.j2 +0 -0
  74. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/fixtures/healthy/projects/project.json.j2 +0 -0
  75. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/02-scenario-directories/rules.yml +0 -0
  76. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/03-sql-gateway/fixtures/analytics/sales_facts.json.j2 +0 -0
  77. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/03-sql-gateway/rules.yml +0 -0
  78. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/04-json-literals/fixtures/orders/search.json.j2 +0 -0
  79. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/04-json-literals/rules.yml +0 -0
  80. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/fixtures/users/archived.json.j2 +0 -0
  81. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/fixtures/users/page.json.j2 +0 -0
  82. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/05-query-parameters/rules.yml +0 -0
  83. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixture_assertions.py +0 -0
  84. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/orders/echo.json.j2 +0 -0
  85. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/projects/project.json.j2 +0 -0
  86. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/fixtures/users/user-1.json.j2 +0 -0
  87. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/06-asserting-in-tests/rules.yml +0 -0
  88. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/07-redirect-mode/fixtures/users/user-1.json.j2 +0 -0
  89. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/07-redirect-mode/rules.yml +0 -0
  90. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-cookbook/upstream.py +0 -0
  91. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/README.md +0 -0
  92. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/fixtures/healthy/analytics/sales_facts.json.j2 +0 -0
  93. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/fixtures/healthy/projects/project.json.j2 +0 -0
  94. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/rules.yml +0 -0
  95. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-eval-isolation/upstream.py +0 -0
  96. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/.env.example +0 -0
  97. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/README.md +0 -0
  98. {mockstack-0.13.0 → mockstack-0.14.0}/examples/proxyrules-with-rules-file/rules.yml +0 -0
  99. {mockstack-0.13.0 → mockstack-0.14.0}/mkdocs.yml +0 -0
  100. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/__init__.py +0 -0
  101. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/exceptions.py +0 -0
  102. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/identifiers.py +0 -0
  103. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/intent.py +0 -0
  104. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/lifespan.py +0 -0
  105. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/llm/__init__.py +0 -0
  106. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/llm/ollama.py +0 -0
  107. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/main.py +0 -0
  108. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/middleware.py +0 -0
  109. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/__init__.py +0 -0
  110. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/catchall.py +0 -0
  111. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/routers/homepage.py +0 -0
  112. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/__init__.py +0 -0
  113. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/base.py +0 -0
  114. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/create_mixin.py +0 -0
  115. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/strategies/factory.py +0 -0
  116. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/telemetry.py +0 -0
  117. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/templating.py +0 -0
  118. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/__init__.py +0 -0
  119. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/conftest.py +0 -0
  120. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/__init__.py +0 -0
  121. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/proxyrules.yml +0 -0
  122. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/templates/__init__.py +0 -0
  123. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/fixtures/templates/example-template.j2 +0 -0
  124. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/__init__.py +0 -0
  125. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_example_eval_isolation.py +0 -0
  126. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_predicates_live.py +0 -0
  127. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/live/test_reverse_proxy_live.py +0 -0
  128. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/llm/__init__.py +0 -0
  129. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/llm/test_ollama.py +0 -0
  130. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/__init__.py +0 -0
  131. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/test_catchall.py +0 -0
  132. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/routers/test_homepage.py +0 -0
  133. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/__init__.py +0 -0
  134. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_create_mixin.py +0 -0
  135. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_filefixtures.py +0 -0
  136. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/strategies/test_filefixtures_example.py +0 -0
  137. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_docs_settings.py +0 -0
  138. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_docs_yaml.py +0 -0
  139. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_identifiers.py +0 -0
  140. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_intent.py +0 -0
  141. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_middleware.py +0 -0
  142. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_telemetry.py +0 -0
  143. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack/tests/test_templating.py +0 -0
  144. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/dependency_links.txt +0 -0
  145. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/entry_points.txt +0 -0
  146. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/requires.txt +0 -0
  147. {mockstack-0.13.0 → mockstack-0.14.0}/mockstack.egg-info/top_level.txt +0 -0
  148. {mockstack-0.13.0 → mockstack-0.14.0}/pyproject.toml +0 -0
  149. {mockstack-0.13.0 → mockstack-0.14.0}/setup.cfg +0 -0
  150. {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/"
@@ -176,3 +176,4 @@ templates/*.j2
176
176
 
177
177
  # rules files rendered from the examples' ${VAR} placeholders
178
178
  examples/**/rules.local.yml
179
+ examples/proxyrules-cookbook/09-recording/fixtures/
@@ -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, reverse-proxies to a real service, or redirects. Rules match on
14
- path, method and `headers`/`query`/`body`/`json` predicates, and every response is
15
- stamped with `X-Mockstack-Result` and `X-Mockstack-Rule`.
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 `make_settings`: it ignores
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`
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Define build argument for version
4
4
  ARG PYTHON_IMAGE_VERSION=3.13-slim
5
- ARG MOCKSTACK_VERSION=0.13.0
5
+ ARG MOCKSTACK_VERSION=0.14.0
6
6
 
7
7
  # ---------------------------- Base Image --------------------------------
8
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockstack
3
- Version: 0.13.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 always
198
- returns HTTP 200 stamped `X-Mockstack-Result: template`; see
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.template_path`: The rendered fixture path, and
421
- `mockstack.proxyrules.result_type` set to `template` (template results)
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 }}}
@@ -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