hecks 3.14.0 → 5.0.0

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 (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -4,11 +4,13 @@
4
4
  use super::keystore::MemoryStore;
5
5
  use super::*;
6
6
  use crate::dispatch::tests::{provision_lineage, scratch_db};
7
+ use crate::host::Host;
7
8
  use crate::lambda_client::NeverInvoker;
8
9
  use crate::web;
9
10
  use axum::extract::State;
10
11
  use axum::http::StatusCode;
11
12
  use axum::response::IntoResponse;
13
+ use std::fmt::Write;
12
14
  use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
13
15
  use std::sync::{Arc, Mutex as StdMutex};
14
16
 
@@ -72,8 +74,12 @@ impl FakeStripe {
72
74
  webhooks_created: AtomicUsize::new(0),
73
75
  fail_deletes: AtomicBool::new(false),
74
76
  });
75
- let app = axum::Router::new().fallback(answer).with_state(state.clone());
76
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind a local port");
77
+ let app = axum::Router::new()
78
+ .fallback(answer)
79
+ .with_state(state.clone());
80
+ let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
81
+ .await
82
+ .expect("bind a local port");
77
83
  let base = format!("http://{}", listener.local_addr().unwrap());
78
84
  tokio::spawn(async move {
79
85
  let _ = axum::serve(listener, app).await;
@@ -86,17 +92,27 @@ impl FakeStripe {
86
92
  }
87
93
 
88
94
  fn requests_to(&self, path: &str) -> Vec<Recorded> {
89
- self.requests().into_iter().filter(|r| r.path == path).collect()
95
+ self.requests()
96
+ .into_iter()
97
+ .filter(|r| r.path == path)
98
+ .collect()
90
99
  }
91
100
  }
92
101
 
93
- async fn answer(State(state): State<Arc<FakeState>>, request: axum::extract::Request) -> axum::response::Response {
102
+ async fn answer(
103
+ State(state): State<Arc<FakeState>>,
104
+ request: axum::extract::Request,
105
+ ) -> axum::response::Response {
94
106
  let (parts, body) = request.into_parts();
95
107
  let bytes = axum::body::to_bytes(body, 1 << 20).await.unwrap();
96
108
  let recorded = Recorded {
97
109
  method: parts.method.to_string(),
98
110
  path: parts.uri.path().to_string(),
99
- headers: parts.headers.iter().map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string())).collect(),
111
+ headers: parts
112
+ .headers
113
+ .iter()
114
+ .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
115
+ .collect(),
100
116
  body: String::from_utf8_lossy(&bytes).to_string(),
101
117
  };
102
118
  let path = recorded.path.clone();
@@ -105,29 +121,58 @@ async fn answer(State(state): State<Arc<FakeState>>, request: axum::extract::Req
105
121
 
106
122
  let (status, reply) = match (method.as_str(), path.as_str()) {
107
123
  // Stripe does not recognise the key: every call it authenticates is a 401.
108
- (_, p) if state.invalid_key.load(Ordering::SeqCst) && p.starts_with("/v1/") => (StatusCode::UNAUTHORIZED, json!({"error": {"message": "Invalid API Key provided: rk_test_****"}})),
124
+ (_, p) if state.invalid_key.load(Ordering::SeqCst) && p.starts_with("/v1/") => (
125
+ StatusCode::UNAUTHORIZED,
126
+ json!({"error": {"message": "Invalid API Key provided: rk_test_****"}}),
127
+ ),
109
128
  // The webhook endpoints of the business's own account.
110
- ("POST", "/v1/webhook_endpoints") if state.refuse_webhooks.load(Ordering::SeqCst) => {
111
- (StatusCode::FORBIDDEN, json!({"error": {"message": "The provided key does not have the required permissions"}}))
112
- }
129
+ ("POST", "/v1/webhook_endpoints") if state.refuse_webhooks.load(Ordering::SeqCst) => (
130
+ StatusCode::FORBIDDEN,
131
+ json!({"error": {"message": "The provided key does not have the required permissions"}}),
132
+ ),
113
133
  ("POST", "/v1/webhook_endpoints") => {
114
134
  let n = state.webhooks_created.fetch_add(1, Ordering::SeqCst) + 1;
115
- (StatusCode::OK, json!({"id": format!("we_{n}"), "object": "webhook_endpoint", "secret": format!("whsec_SAVED_{n}")}))
135
+ (
136
+ StatusCode::OK,
137
+ json!({"id": format!("we_{n}"), "object": "webhook_endpoint", "secret": format!("whsec_SAVED_{n}")}),
138
+ )
116
139
  }
117
- ("DELETE", p) if p.starts_with("/v1/webhook_endpoints/") && state.fail_deletes.load(Ordering::SeqCst) => {
118
- (StatusCode::INTERNAL_SERVER_ERROR, json!({"error": {"message": "Stripe is having trouble"}}))
140
+ ("DELETE", p)
141
+ if p.starts_with("/v1/webhook_endpoints/")
142
+ && state.fail_deletes.load(Ordering::SeqCst) =>
143
+ {
144
+ (
145
+ StatusCode::INTERNAL_SERVER_ERROR,
146
+ json!({"error": {"message": "Stripe is having trouble"}}),
147
+ )
119
148
  }
120
- ("DELETE", p) if p.starts_with("/v1/webhook_endpoints/") => (StatusCode::OK, json!({"id": p.trim_start_matches("/v1/webhook_endpoints/"), "deleted": true})),
149
+ ("DELETE", p) if p.starts_with("/v1/webhook_endpoints/") => (
150
+ StatusCode::OK,
151
+ json!({"id": p.trim_start_matches("/v1/webhook_endpoints/"), "deleted": true}),
152
+ ),
121
153
  // The business's own account, read with its own key.
122
- ("GET", "/v1/account") if state.refuse_account.load(Ordering::SeqCst) => (StatusCode::FORBIDDEN, json!({"error": {"message": "key lacks permission"}})),
123
- ("GET", "/v1/account") => (StatusCode::OK, json!({"id": "acct_own_1", "business_profile": {"name": "Own Studio"}})),
124
- ("POST", "/v1/checkout/sessions") if state.refuse_sessions.load(Ordering::SeqCst) => {
125
- (StatusCode::BAD_REQUEST, json!({"error": {"message": "no such price"}}))
126
- }
154
+ ("GET", "/v1/account") if state.refuse_account.load(Ordering::SeqCst) => (
155
+ StatusCode::FORBIDDEN,
156
+ json!({"error": {"message": "key lacks permission"}}),
157
+ ),
158
+ ("GET", "/v1/account") => (
159
+ StatusCode::OK,
160
+ json!({"id": "acct_own_1", "business_profile": {"name": "Own Studio"}}),
161
+ ),
162
+ ("POST", "/v1/checkout/sessions") if state.refuse_sessions.load(Ordering::SeqCst) => (
163
+ StatusCode::BAD_REQUEST,
164
+ json!({"error": {"message": "no such price"}}),
165
+ ),
127
166
  // An embedded session: Stripe answers with a client secret for the
128
167
  // browser and no hosted `url`.
129
- ("POST", "/v1/checkout/sessions") => (StatusCode::OK, json!({"id": "cs_test_1", "ui_mode": "embedded_page", "client_secret": "cs_test_1_secret_CLIENT"})),
130
- _ => (StatusCode::NOT_FOUND, json!({"error": {"message": "no such fake route"}})),
168
+ ("POST", "/v1/checkout/sessions") => (
169
+ StatusCode::OK,
170
+ json!({"id": "cs_test_1", "ui_mode": "embedded_page", "client_secret": "cs_test_1_secret_CLIENT"}),
171
+ ),
172
+ _ => (
173
+ StatusCode::NOT_FOUND,
174
+ json!({"error": {"message": "no such fake route"}}),
175
+ ),
131
176
  };
132
177
  (status, axum::Json(reply)).into_response()
133
178
  }
@@ -147,7 +192,13 @@ async fn tenant(name: &str) -> Tenant {
147
192
  let client = scratch_db(name).await;
148
193
  {
149
194
  let guard = client.lock().await;
150
- provision_lineage(&guard, "CheckoutFixture", 1, &["Event", "Registration", "Payment", "PaymentConnection"]).await;
195
+ provision_lineage(
196
+ &guard,
197
+ "CheckoutFixture",
198
+ 1,
199
+ &["Event", "Registration", "Payment", "PaymentConnection"],
200
+ )
201
+ .await;
151
202
  guard
152
203
  .batch_execute(
153
204
  "CREATE TABLE acme_member_head_snapshot_1 (id text PRIMARY KEY, ordinal bigint NOT NULL, state jsonb NOT NULL);
@@ -202,8 +253,13 @@ async fn tenant(name: &str) -> Tenant {
202
253
  Tenant {
203
254
  client,
204
255
  domain_ir,
205
- config: LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None },
206
- wasm: std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/checkout_fixture.wasm"),
256
+ config: LineageConfig {
257
+ domain: "CheckoutFixture".to_string(),
258
+ era: Some(1),
259
+ mirrored: None,
260
+ },
261
+ wasm: std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
262
+ .join("../dist/checkout_fixture.wasm"),
207
263
  fake,
208
264
  platform,
209
265
  raw: None,
@@ -221,7 +277,7 @@ impl Tenant {
221
277
 
222
278
  // A tenant whose business can paste its keys into the Payments page: no
223
279
  // environment keys, only a key store (in memory).
224
- async fn saving(name: &str) -> Tenant {
280
+ async fn saving(name: &str) -> Self {
225
281
  let mut t = tenant(name).await;
226
282
  t.platform.direct_test_key = String::new();
227
283
  t.platform.direct_test_publishable_key = String::new();
@@ -232,49 +288,98 @@ impl Tenant {
232
288
  }
233
289
 
234
290
  // The Payments page's Save, as the given person.
235
- async fn save_keys(&self, secret_key: &str, publishable_key: &str, as_email: Option<&str>) -> (u64, Value) {
236
- self.call("POST", "/payments/connection/direct", json!({"secret_key": secret_key, "publishable_key": publishable_key}), as_email).await
291
+ async fn save_keys(
292
+ &self,
293
+ secret_key: &str,
294
+ publishable_key: &str,
295
+ as_email: Option<&str>,
296
+ ) -> (u64, Value) {
297
+ self.call(
298
+ "POST",
299
+ "/payments/connection/direct",
300
+ json!({"secret_key": secret_key, "publishable_key": publishable_key}),
301
+ as_email,
302
+ )
303
+ .await
237
304
  }
238
305
 
239
306
  // What the saved-keys secret holds right now (`Null` before the first save).
240
307
  fn saved_document(&self) -> Value {
241
308
  let raw = self.raw.as_ref().expect("a tenant with a key store");
242
309
  let text = raw.document.lock().unwrap().clone();
243
- text.map(|text| serde_json::from_str(&text).unwrap()).unwrap_or(Value::Null)
310
+ text.map_or(Value::Null, |text| serde_json::from_str(&text).unwrap())
244
311
  }
245
312
 
246
313
  // One request against the payments routes; `None` means no session cookie.
247
- async fn call(&self, method: &str, path: &str, body: Value, as_email: Option<&str>) -> (u64, Value) {
314
+ async fn call(
315
+ &self,
316
+ method: &str,
317
+ path: &str,
318
+ body: Value,
319
+ as_email: Option<&str>,
320
+ ) -> (u64, Value) {
248
321
  let mut cookies = HashMap::new();
249
322
  if let Some(email) = as_email {
250
- cookies.insert(auth::account_cookie_name(), auth::account_token(SESSION_SECRET, email, 60));
323
+ cookies.insert(
324
+ auth::account_cookie_name(),
325
+ auth::account_token(SESSION_SECRET, email, 60),
326
+ );
251
327
  }
252
328
  let platform = self.platform_now().await;
253
- let response = route(method, path, &body.to_string(), &cookies, SESSION_SECRET, &self.domain_ir, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker)
254
- .await
255
- .expect("a payments path");
256
- (response["statusCode"].as_u64().unwrap(), serde_json::from_str(response["body"].as_str().unwrap()).unwrap_or(Value::Null))
329
+ let response = route(
330
+ method,
331
+ path,
332
+ &body.to_string(),
333
+ &cookies,
334
+ SESSION_SECRET,
335
+ &self.domain_ir,
336
+ &platform,
337
+ Host::new(&self.client, &self.wasm, &self.config, &NeverInvoker),
338
+ )
339
+ .await
340
+ .expect("a payments path");
341
+ (
342
+ response["statusCode"].as_u64().unwrap(),
343
+ serde_json::from_str(response["body"].as_str().unwrap()).unwrap_or(Value::Null),
344
+ )
257
345
  }
258
346
 
259
347
  async fn status(&self) -> String {
260
348
  let read = dispatch::read(&self.client, &self.wasm).await.unwrap();
261
- connection(&read, &self.config.domain).map(|c| c.status).unwrap_or_else(|| "none".to_string())
349
+ connection(&read, &self.config.domain).map_or_else(|| "none".to_string(), |c| c.status)
262
350
  }
263
351
 
264
352
  // Owner records the business's own test-mode account as the connection.
265
353
  async fn connect(&self) {
266
- let (status, body) = self.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
354
+ let (status, body) = self
355
+ .call(
356
+ "POST",
357
+ "/payments/connection/direct",
358
+ json!({"mode": "test"}),
359
+ Some(OWNER),
360
+ )
361
+ .await;
267
362
  assert_eq!(status, 200, "{body}");
268
363
  }
269
364
 
270
365
  // Every row of every table in the scratch database, as text.
271
366
  async fn dump_database(&self) -> String {
272
367
  let guard = self.client.lock().await;
273
- let tables = guard.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'", &[]).await.unwrap();
368
+ let tables = guard
369
+ .query(
370
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
371
+ &[],
372
+ )
373
+ .await
374
+ .unwrap();
274
375
  let mut dump = String::new();
275
376
  for table in tables {
276
377
  let name: String = table.get(0);
277
- for row in guard.query(&format!("SELECT t::text FROM \"{name}\" t"), &[]).await.unwrap() {
378
+ for row in guard
379
+ .query(&format!("SELECT t::text FROM \"{name}\" t"), &[])
380
+ .await
381
+ .unwrap()
382
+ {
278
383
  dump.push_str(&row.get::<_, String>(0));
279
384
  dump.push('\n');
280
385
  }
@@ -283,7 +388,14 @@ impl Tenant {
283
388
  }
284
389
 
285
390
  async fn enable(&self) {
286
- let (status, body) = self.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
391
+ let (status, body) = self
392
+ .call(
393
+ "POST",
394
+ "/payments/connection/enable",
395
+ json!({}),
396
+ Some(OPERATOR),
397
+ )
398
+ .await;
287
399
  assert_eq!(status, 200, "{body}");
288
400
  }
289
401
 
@@ -293,16 +405,33 @@ impl Tenant {
293
405
 
294
406
  async fn schedule_event_with_capacity(&self, slug: &str, capacity: i64) {
295
407
  let args = json!({"slug": {"value": slug}, "name": {"value": "Sample Studio"}, "price": {"cents": 4200}, "capacity": {"value": capacity}});
296
- let outcome = dispatch::handle(&self.client, &self.wasm, "CheckoutFixture::Event.Schedule", args, None, &self.config, &NeverInvoker).await.unwrap();
408
+ let outcome = dispatch::handle(
409
+ Host::new(&self.client, &self.wasm, &self.config, &NeverInvoker),
410
+ "CheckoutFixture::Event.Schedule",
411
+ args,
412
+ None,
413
+ )
414
+ .await
415
+ .unwrap();
297
416
  assert!(outcome.accepted, "{:?}", outcome.result);
298
417
  }
299
418
 
300
419
  // POST /registrations for `slug`; the checkout URL and registration id on success.
301
420
  async fn register(&self, slug: &str) -> (u64, Value) {
302
- let body = json!({"event_slug": slug, "name": "Ada Lovelace", "email": "ada@example.com"}).to_string();
421
+ let body = json!({"event_slug": slug, "name": "Ada Lovelace", "email": "ada@example.com"})
422
+ .to_string();
303
423
  let platform = self.platform_now().await;
304
- let response = web::registrations_route(&body, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
305
- (response["statusCode"].as_u64().unwrap(), serde_json::from_str(response["body"].as_str().unwrap()).unwrap_or(Value::Null))
424
+ let response = web::registrations_route(
425
+ &body,
426
+ &platform,
427
+ Host::new(&self.client, &self.wasm, &self.config, &NeverInvoker),
428
+ &crate::commerce_ir::fixture_payments(),
429
+ )
430
+ .await;
431
+ (
432
+ response["statusCode"].as_u64().unwrap(),
433
+ serde_json::from_str(response["body"].as_str().unwrap()).unwrap_or(Value::Null),
434
+ )
306
435
  }
307
436
 
308
437
  async fn webhook(&self, event: Value) -> u64 {
@@ -312,23 +441,42 @@ impl Tenant {
312
441
  // A webhook delivery signed with `secret`, the way Stripe would sign it.
313
442
  async fn webhook_signed_with(&self, secret: &str, event: Value) -> u64 {
314
443
  let payload = event.to_string();
315
- let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
444
+ let now = std::time::SystemTime::now()
445
+ .duration_since(std::time::UNIX_EPOCH)
446
+ .unwrap()
447
+ .as_secs() as i64;
316
448
  let header = sign(secret, now, &payload);
317
449
  let platform = self.platform_now().await;
318
- let response = web::webhook_route(&payload, &header, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
450
+ let response = web::webhook_route(
451
+ &payload,
452
+ &header,
453
+ &platform,
454
+ Host::new(&self.client, &self.wasm, &self.config, &NeverInvoker),
455
+ &crate::commerce_ir::fixture_payments(),
456
+ )
457
+ .await;
319
458
  response["statusCode"].as_u64().unwrap()
320
459
  }
321
460
 
322
461
  // POST /registrations/:id/complete with a successful outcome (the mock
323
462
  // walkthrough's "Pay" button).
324
463
  async fn settle(&self, reference: &str) -> u64 {
325
- let response = web::registration_complete_route(reference, r#"{"outcome":"succeeded"}"#, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
464
+ let response = web::registration_complete_route(
465
+ reference,
466
+ r#"{"outcome":"succeeded"}"#,
467
+ Host::new(&self.client, &self.wasm, &self.config, &NeverInvoker),
468
+ &crate::commerce_ir::fixture_payments(),
469
+ )
470
+ .await;
326
471
  response["statusCode"].as_u64().unwrap()
327
472
  }
328
473
 
329
474
  async fn payment_status(&self, reference: &str) -> String {
330
475
  let read = dispatch::read(&self.client, &self.wasm).await.unwrap();
331
- read["instances"][format!("Payments::Payment#{reference}")]["status"].as_str().unwrap_or("none").to_string()
476
+ read["instances"][format!("Payments::Payment#{reference}")]["status"]
477
+ .as_str()
478
+ .unwrap_or("none")
479
+ .to_string()
332
480
  }
333
481
  }
334
482
 
@@ -337,7 +485,15 @@ fn sign(secret: &str, now: i64, payload: &str) -> String {
337
485
  use sha2::Sha256;
338
486
  let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
339
487
  mac.update(format!("{now}.{payload}").as_bytes());
340
- format!("t={now},v1={}", mac.finalize().into_bytes().iter().map(|b| format!("{b:02x}")).collect::<String>())
488
+ let digest = mac
489
+ .finalize()
490
+ .into_bytes()
491
+ .iter()
492
+ .fold(String::new(), |mut hex, b| {
493
+ let _ = write!(hex, "{b:02x}"); // writing to a String cannot fail
494
+ hex
495
+ });
496
+ format!("t={now},v1={digest}")
341
497
  }
342
498
 
343
499
  fn completed(reference: &str) -> Value {
@@ -359,9 +515,16 @@ async fn every_route_refuses_a_missing_session_with_a_json_401_and_touches_nothi
359
515
  ("POST", "/payments/connection/disable"),
360
516
  ] {
361
517
  let (status, body) = t.call(method, path, json!({}), None).await;
362
- assert_eq!((status, body["error"].as_str()), (401, Some("not logged in")), "{method} {path}");
518
+ assert_eq!(
519
+ (status, body["error"].as_str()),
520
+ (401, Some("not logged in")),
521
+ "{method} {path}"
522
+ );
363
523
  }
364
- assert!(t.fake.requests().is_empty(), "no Stripe call without a session");
524
+ assert!(
525
+ t.fake.requests().is_empty(),
526
+ "no Stripe call without a session"
527
+ );
365
528
  assert_eq!(t.status().await, "none");
366
529
  }
367
530
 
@@ -369,14 +532,42 @@ async fn every_route_refuses_a_missing_session_with_a_json_401_and_touches_nothi
369
532
  async fn a_tampered_or_disabled_session_is_a_401_not_an_owner() {
370
533
  let t = tenant("hecks_pay_test_401_disabled").await;
371
534
  let mut cookies = HashMap::new();
372
- cookies.insert(auth::account_cookie_name(), "garbage.notasignature".to_string());
373
- let response = route("GET", "/payments/connection", "", &cookies, SESSION_SECRET, &t.domain_ir, &t.platform, &t.client, &t.wasm, &t.config, &NeverInvoker).await.unwrap();
535
+ cookies.insert(
536
+ auth::account_cookie_name(),
537
+ "garbage.notasignature".to_string(),
538
+ );
539
+ let response = route(
540
+ "GET",
541
+ "/payments/connection",
542
+ "",
543
+ &cookies,
544
+ SESSION_SECRET,
545
+ &t.domain_ir,
546
+ &t.platform,
547
+ Host::new(&t.client, &t.wasm, &t.config, &NeverInvoker),
548
+ )
549
+ .await
550
+ .unwrap();
374
551
  assert_eq!(response["statusCode"], 401);
375
552
 
376
553
  // An Owner disabled after the cookie was signed: the signature is still valid.
377
- let (status, _) = t.call("GET", "/payments/connection", json!({}), Some(DISABLED_OWNER)).await;
554
+ let (status, _) = t
555
+ .call(
556
+ "GET",
557
+ "/payments/connection",
558
+ json!({}),
559
+ Some(DISABLED_OWNER),
560
+ )
561
+ .await;
378
562
  assert_eq!(status, 401);
379
- let (status, _) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(DISABLED_OWNER)).await;
563
+ let (status, _) = t
564
+ .call(
565
+ "POST",
566
+ "/payments/connection/direct",
567
+ json!({"mode": "test"}),
568
+ Some(DISABLED_OWNER),
569
+ )
570
+ .await;
380
571
  assert_eq!(status, 401);
381
572
  }
382
573
 
@@ -385,30 +576,60 @@ async fn only_an_owner_may_connect_or_disconnect_and_only_the_operator_may_enabl
385
576
  let t = tenant("hecks_pay_test_roles").await;
386
577
 
387
578
  // A non-Owner admin: read refused, every owner action refused.
388
- let (status, _) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
579
+ let (status, _) = t
580
+ .call("GET", "/payments/connection", json!({}), Some(ADMIN))
581
+ .await;
389
582
  assert_eq!(status, 403);
390
- for path in ["/payments/connection/direct", "/payments/connection/disconnect"] {
391
- let (status, body) = t.call("POST", path, json!({"mode": "test"}), Some(ADMIN)).await;
392
- assert_eq!((status, body["error"].as_str()), (403, Some("only an Owner can manage payments")), "{path}");
583
+ for path in [
584
+ "/payments/connection/direct",
585
+ "/payments/connection/disconnect",
586
+ ] {
587
+ let (status, body) = t
588
+ .call("POST", path, json!({"mode": "test"}), Some(ADMIN))
589
+ .await;
590
+ assert_eq!(
591
+ (status, body["error"].as_str()),
592
+ (403, Some("only an Owner can manage payments")),
593
+ "{path}"
594
+ );
393
595
  }
394
596
 
395
597
  t.connect().await;
396
598
  assert_eq!(t.status().await, "connected");
397
599
 
398
600
  // The Owner is not the operator: enable and disable are refused.
399
- for path in ["/payments/connection/enable", "/payments/connection/disable"] {
601
+ for path in [
602
+ "/payments/connection/enable",
603
+ "/payments/connection/disable",
604
+ ] {
400
605
  let (status, _) = t.call("POST", path, json!({}), Some(OWNER)).await;
401
606
  assert_eq!(status, 403, "{path}");
402
607
  }
403
608
  // The operator is not an Owner: cannot disconnect.
404
- let (status, _) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OPERATOR)).await;
609
+ let (status, _) = t
610
+ .call(
611
+ "POST",
612
+ "/payments/connection/disconnect",
613
+ json!({}),
614
+ Some(OPERATOR),
615
+ )
616
+ .await;
405
617
  assert_eq!(status, 403);
406
- assert_eq!(t.status().await, "connected", "nothing above changed the connection");
618
+ assert_eq!(
619
+ t.status().await,
620
+ "connected",
621
+ "nothing above changed the connection"
622
+ );
407
623
 
408
624
  // ...but the operator may read the page state, and enable.
409
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(OPERATOR)).await;
625
+ let (status, body) = t
626
+ .call("GET", "/payments/connection", json!({}), Some(OPERATOR))
627
+ .await;
410
628
  assert_eq!(status, 200);
411
- assert_eq!((body["can_manage"].clone(), body["can_enable"].clone()), (json!(false), json!(true)));
629
+ assert_eq!(
630
+ (body["can_manage"].clone(), body["can_enable"].clone()),
631
+ (json!(false), json!(true))
632
+ );
412
633
  t.enable().await;
413
634
  assert_eq!(t.status().await, "enabled");
414
635
  }
@@ -418,103 +639,258 @@ async fn a_site_that_lists_admin_as_a_payments_manager_lets_an_admin_manage() {
418
639
  let mut t = tenant("hecks_pay_test_manager_roles").await;
419
640
  t.platform.manager_roles = vec!["Owner".to_string(), "Admin".to_string()];
420
641
 
421
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
422
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
642
+ let (status, body) = t
643
+ .call("GET", "/payments/connection", json!({}), Some(ADMIN))
644
+ .await;
645
+ assert_eq!(
646
+ (status, body["can_manage"].clone()),
647
+ (200, json!(true)),
648
+ "{body}"
649
+ );
423
650
  t.connect().await;
424
651
  assert_eq!(t.status().await, "connected");
425
- let (status, _) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(ADMIN)).await;
652
+ let (status, _) = t
653
+ .call(
654
+ "POST",
655
+ "/payments/connection/disconnect",
656
+ json!({}),
657
+ Some(ADMIN),
658
+ )
659
+ .await;
426
660
  assert_eq!(status, 200);
427
661
  assert_eq!(t.status().await, "disconnected");
428
662
 
429
663
  // Enabling stays with the operator, and the Owner still manages.
430
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
431
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
664
+ let (status, body) = t
665
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
666
+ .await;
667
+ assert_eq!(
668
+ (status, body["can_manage"].clone()),
669
+ (200, json!(true)),
670
+ "{body}"
671
+ );
432
672
  // Someone signed out, or not listed, is refused with today's message.
433
673
  let (status, _) = t.call("GET", "/payments/connection", json!({}), None).await;
434
674
  assert_eq!(status, 401);
435
675
  t.platform.manager_roles = vec!["Editor".to_string()];
436
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
437
- assert_eq!((status, body["error"].as_str()), (403, Some("only an Owner can manage payments")));
676
+ let (status, body) = t
677
+ .call(
678
+ "POST",
679
+ "/payments/connection/direct",
680
+ json!({"mode": "test"}),
681
+ Some(OWNER),
682
+ )
683
+ .await;
684
+ assert_eq!(
685
+ (status, body["error"].as_str()),
686
+ (403, Some("only an Owner can manage payments"))
687
+ );
438
688
  }
439
689
 
440
690
  #[tokio::test]
441
691
  async fn an_admin_granted_owner_passes_the_payments_gate_and_keeps_the_admin_gate() {
442
692
  let t = tenant("hecks_pay_test_owner_bootstrap").await;
443
- let members = LineageConfig { domain: "Acme".to_string(), era: Some(1), mirrored: None };
444
- let cookies = |email: &str| HashMap::from([(auth::account_cookie_name(), auth::account_token(SESSION_SECRET, email, 60))]);
693
+ let members = LineageConfig {
694
+ domain: "Acme".to_string(),
695
+ era: Some(1),
696
+ mirrored: None,
697
+ };
698
+ let cookies = |email: &str| {
699
+ HashMap::from([(
700
+ auth::account_cookie_name(),
701
+ auth::account_token(SESSION_SECRET, email, 60),
702
+ )])
703
+ };
445
704
 
446
705
  // An Admin is not an Owner: the payments routes refuse them.
447
- let (status, _) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
706
+ let (status, _) = t
707
+ .call("GET", "/payments/connection", json!({}), Some(ADMIN))
708
+ .await;
448
709
  assert_eq!(status, 403);
449
710
 
450
711
  // An Admin grants Owner to a new person; that person passes the Payments
451
712
  // Owner gate and is still admitted by the members admin gate.
452
713
  let body = json!({"email": "boss@example.com", "name": "Boss", "role": "Owner"}).to_string();
453
- let response = web::add_member_route(&t.domain_ir, &body, &cookies(ADMIN), SESSION_SECRET, &t.client, &t.wasm, &members).await;
714
+ let response = web::add_member_route(
715
+ &t.domain_ir,
716
+ &body,
717
+ &cookies(ADMIN),
718
+ SESSION_SECRET,
719
+ &t.client,
720
+ &t.wasm,
721
+ &members,
722
+ )
723
+ .await;
454
724
  assert_eq!(response["statusCode"], 201, "{response:?}");
455
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some("boss@example.com")).await;
456
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
457
- let response = web::add_member_route(&t.domain_ir, r#"{"email": "next@example.com", "name": "Next"}"#, &cookies("boss@example.com"), SESSION_SECRET, &t.client, &t.wasm, &members).await;
725
+ let (status, body) = t
726
+ .call(
727
+ "GET",
728
+ "/payments/connection",
729
+ json!({}),
730
+ Some("boss@example.com"),
731
+ )
732
+ .await;
733
+ assert_eq!(
734
+ (status, body["can_manage"].clone()),
735
+ (200, json!(true)),
736
+ "{body}"
737
+ );
738
+ let response = web::add_member_route(
739
+ &t.domain_ir,
740
+ r#"{"email": "next@example.com", "name": "Next"}"#,
741
+ &cookies("boss@example.com"),
742
+ SESSION_SECRET,
743
+ &t.client,
744
+ &t.wasm,
745
+ &members,
746
+ )
747
+ .await;
458
748
  assert_eq!(response["statusCode"], 201, "{response:?}");
459
749
 
460
750
  // Granting Owner to an existing Admin turns them into an Owner too.
461
- assert!(auth::grant_access(&t.client, &t.wasm, &members, &t.domain_ir, ADMIN, "Owner").await.unwrap());
462
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
463
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
464
- assert!(auth::caller_is_admin(&t.client, &t.domain_ir, ADMIN).await.unwrap());
751
+ assert!(
752
+ auth::grant_access(&t.client, &t.wasm, &members, &t.domain_ir, ADMIN, "Owner")
753
+ .await
754
+ .unwrap()
755
+ );
756
+ let (status, body) = t
757
+ .call("GET", "/payments/connection", json!({}), Some(ADMIN))
758
+ .await;
759
+ assert_eq!(
760
+ (status, body["can_manage"].clone()),
761
+ (200, json!(true)),
762
+ "{body}"
763
+ );
764
+ assert!(auth::caller_is_admin(&t.client, &t.domain_ir, ADMIN)
765
+ .await
766
+ .unwrap());
465
767
  }
466
768
 
467
769
  #[tokio::test]
468
770
  async fn the_status_route_reports_not_connected_and_which_modes_have_keys() {
469
771
  let t = tenant("hecks_pay_test_status_empty").await;
470
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
772
+ let (status, body) = t
773
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
774
+ .await;
471
775
  assert_eq!(status, 200);
472
776
  assert_eq!(body["status"], "not_connected");
473
777
  assert_eq!(body["processor"], Value::Null);
474
- assert_eq!((body["direct_modes"].clone(), body["direct"].clone(), body["adapters"].clone()), (json!(["test"]), json!(false), json!([])));
475
- assert_eq!((body["can_manage"].clone(), body["can_enable"].clone()), (json!(true), json!(false)));
778
+ assert_eq!(
779
+ (
780
+ body["direct_modes"].clone(),
781
+ body["direct"].clone(),
782
+ body["adapters"].clone()
783
+ ),
784
+ (json!(["test"]), json!(false), json!([]))
785
+ );
786
+ assert_eq!(
787
+ (body["can_manage"].clone(), body["can_enable"].clone()),
788
+ (json!(true), json!(false))
789
+ );
476
790
 
477
791
  // With no keys at all, no mode is offered.
478
792
  let mut bare = tenant("hecks_pay_test_status_bare").await;
479
793
  bare.platform.direct_test_key = String::new();
480
- let (_, body) = bare.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
794
+ let (_, body) = bare
795
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
796
+ .await;
481
797
  assert_eq!(body["direct_modes"], json!([]));
482
798
  }
483
799
 
484
800
  #[tokio::test]
485
801
  async fn enable_needs_a_connection_and_follows_the_lifecycle() {
486
802
  let t = tenant("hecks_pay_test_enable").await;
487
- let (status, body) = t.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
488
- assert_eq!((status, body["error"].as_str()), (409, Some("connect an account first")));
803
+ let (status, body) = t
804
+ .call(
805
+ "POST",
806
+ "/payments/connection/enable",
807
+ json!({}),
808
+ Some(OPERATOR),
809
+ )
810
+ .await;
811
+ assert_eq!(
812
+ (status, body["error"].as_str()),
813
+ (409, Some("connect an account first"))
814
+ );
489
815
 
490
816
  t.connect().await;
491
- let (status, body) = t.call("POST", "/payments/connection/disable", json!({}), Some(OPERATOR)).await;
492
- assert_eq!(status, 422, "disable is refused unless payments are enabled: {body}");
817
+ let (status, body) = t
818
+ .call(
819
+ "POST",
820
+ "/payments/connection/disable",
821
+ json!({}),
822
+ Some(OPERATOR),
823
+ )
824
+ .await;
825
+ assert_eq!(
826
+ status, 422,
827
+ "disable is refused unless payments are enabled: {body}"
828
+ );
493
829
 
494
830
  t.enable().await;
495
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
831
+ let (status, body) = t
832
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
833
+ .await;
496
834
  assert_eq!((status, body["status"].as_str()), (200, Some("enabled")));
497
- let (status, _) = t.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
835
+ let (status, _) = t
836
+ .call(
837
+ "POST",
838
+ "/payments/connection/enable",
839
+ json!({}),
840
+ Some(OPERATOR),
841
+ )
842
+ .await;
498
843
  assert_eq!(status, 422, "enable is refused when already enabled");
499
844
 
500
- let (status, body) = t.call("POST", "/payments/connection/disable", json!({}), Some(OPERATOR)).await;
845
+ let (status, body) = t
846
+ .call(
847
+ "POST",
848
+ "/payments/connection/disable",
849
+ json!({}),
850
+ Some(OPERATOR),
851
+ )
852
+ .await;
501
853
  assert_eq!((status, body["status"].as_str()), (200, Some("connected")));
502
854
  }
503
855
 
504
856
  #[tokio::test]
505
857
  async fn disconnecting_a_not_yet_enabled_account_unlinks_it_and_it_can_be_reconnected() {
506
858
  let t = tenant("hecks_pay_test_disconnect").await;
507
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
508
- assert_eq!((status, body["error"].as_str()), (409, Some("nothing to disconnect")));
859
+ let (status, body) = t
860
+ .call(
861
+ "POST",
862
+ "/payments/connection/disconnect",
863
+ json!({}),
864
+ Some(OWNER),
865
+ )
866
+ .await;
867
+ assert_eq!(
868
+ (status, body["error"].as_str()),
869
+ (409, Some("nothing to disconnect"))
870
+ );
509
871
 
510
872
  t.connect().await;
511
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
873
+ let (status, body) = t
874
+ .call(
875
+ "POST",
876
+ "/payments/connection/disconnect",
877
+ json!({}),
878
+ Some(OWNER),
879
+ )
880
+ .await;
512
881
  assert_eq!(status, 200, "{body}");
513
882
  assert_eq!(body["status"], "disconnected");
514
- assert!(t.fake.requests().iter().all(|r| r.method != "DELETE"), "there is no webhook to remove for keys from the environment");
883
+ assert!(
884
+ t.fake.requests().iter().all(|r| r.method != "DELETE"),
885
+ "there is no webhook to remove for keys from the environment"
886
+ );
515
887
 
516
888
  t.connect().await;
517
- assert_eq!(t.status().await, "connected", "a disconnected link reconnects");
889
+ assert_eq!(
890
+ t.status().await,
891
+ "connected",
892
+ "a disconnected link reconnects"
893
+ );
518
894
  }
519
895
 
520
896
  #[tokio::test]
@@ -523,16 +899,34 @@ async fn disconnecting_while_payments_are_enabled_pauses_them() {
523
899
  t.connect().await;
524
900
  t.enable().await;
525
901
 
526
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
902
+ let (status, body) = t
903
+ .call(
904
+ "POST",
905
+ "/payments/connection/disconnect",
906
+ json!({}),
907
+ Some(OWNER),
908
+ )
909
+ .await;
527
910
  assert_eq!(status, 200, "{body}");
528
911
  assert_eq!(body["status"], "paused");
529
912
 
530
913
  // Paused is not disconnected: it cannot be disconnected again, and a new
531
914
  // connection resumes it instead of starting over.
532
- let (status, _) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
915
+ let (status, _) = t
916
+ .call(
917
+ "POST",
918
+ "/payments/connection/disconnect",
919
+ json!({}),
920
+ Some(OWNER),
921
+ )
922
+ .await;
533
923
  assert_eq!(status, 409);
534
924
  t.connect().await;
535
- assert_eq!(t.status().await, "enabled", "reconnecting a paused connection resumes it");
925
+ assert_eq!(
926
+ t.status().await,
927
+ "enabled",
928
+ "reconnecting a paused connection resumes it"
929
+ );
536
930
  }
537
931
 
538
932
  #[tokio::test]
@@ -543,17 +937,35 @@ async fn checkout_stays_on_the_mock_until_payments_are_enabled() {
543
937
  // No connection at all.
544
938
  let (status, body) = t.register("mock-event").await;
545
939
  assert_eq!(status, 200, "{body}");
546
- assert!(body["checkout_url"].as_str().unwrap().starts_with("https://site.example/pay/"), "{body}");
940
+ assert!(
941
+ body["checkout_url"]
942
+ .as_str()
943
+ .unwrap()
944
+ .starts_with("https://site.example/pay/"),
945
+ "{body}"
946
+ );
547
947
 
548
948
  // Connected but not enabled: still the mock.
549
949
  t.connect().await;
550
950
  let (status, body) = t.register("mock-event").await;
551
951
  assert_eq!(status, 200, "{body}");
552
- assert!(body["checkout_url"].as_str().unwrap().starts_with("https://site.example/pay/"), "{body}");
553
- assert!(t.fake.requests_to("/v1/checkout/sessions").is_empty(), "the mock never calls Stripe");
952
+ assert!(
953
+ body["checkout_url"]
954
+ .as_str()
955
+ .unwrap()
956
+ .starts_with("https://site.example/pay/"),
957
+ "{body}"
958
+ );
959
+ assert!(
960
+ t.fake.requests_to("/v1/checkout/sessions").is_empty(),
961
+ "the mock never calls Stripe"
962
+ );
554
963
 
555
964
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
556
- let processors: Vec<String> = instances_for(&read, "Payments::Payment#").iter().map(|(_, p)| p["processor"]["value"].as_str().unwrap().to_string()).collect();
965
+ let processors: Vec<String> = instances_for(&read, "Payments::Payment#")
966
+ .iter()
967
+ .map(|(_, p)| p["processor"]["value"].as_str().unwrap().to_string())
968
+ .collect();
557
969
  assert_eq!(processors, vec!["mock_stripe", "mock_stripe"]);
558
970
  }
559
971
 
@@ -568,7 +980,11 @@ async fn an_enabled_connection_charges_on_the_business_own_account() {
568
980
  assert_eq!(status, 200, "{body}");
569
981
  // The guest pays in a form embedded in the site: what the browser needs to
570
982
  // mount it, and no hosted page to send them to.
571
- assert_eq!(body.get("checkout_url"), None, "nothing leaves the site: {body}");
983
+ assert_eq!(
984
+ body.get("checkout_url"),
985
+ None,
986
+ "nothing leaves the site: {body}"
987
+ );
572
988
  assert_eq!(
573
989
  body["embedded_checkout"],
574
990
  json!({
@@ -581,17 +997,54 @@ async fn an_enabled_connection_charges_on_the_business_own_account() {
581
997
 
582
998
  let sent = t.fake.requests_to("/v1/checkout/sessions");
583
999
  assert_eq!(sent.len(), 1);
584
- assert_eq!(sent[0].header("stripe-account"), None, "the business's own account is the default account");
585
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {DIRECT_KEY}").as_str()), "authenticated with the account's own key");
586
- assert_eq!(sent[0].header("stripe-version"), Some("2026-04-22.dahlia"), "pinned, not the account's default");
587
- assert!(sent[0].body.contains(&format!("metadata%5Bregistration_id%5D={reference}")), "{}", sent[0].body);
588
- assert!(sent[0].body.contains("unit_amount%5D=4200"), "{}", sent[0].body);
589
- assert!(sent[0].body.contains("ui_mode=embedded_page"), "an embedded session, not a hosted one: {}", sent[0].body);
590
- assert!(sent[0].body.contains("redirect_on_completion=never"), "{}", sent[0].body);
591
- assert!(!sent[0].body.contains("success_url") && !sent[0].body.contains("cancel_url"), "no return pages exist: {}", sent[0].body);
1000
+ assert_eq!(
1001
+ sent[0].header("stripe-account"),
1002
+ None,
1003
+ "the business's own account is the default account"
1004
+ );
1005
+ assert_eq!(
1006
+ sent[0].header("authorization"),
1007
+ Some(format!("Bearer {DIRECT_KEY}").as_str()),
1008
+ "authenticated with the account's own key"
1009
+ );
1010
+ assert_eq!(
1011
+ sent[0].header("stripe-version"),
1012
+ Some("2026-04-22.dahlia"),
1013
+ "pinned, not the account's default"
1014
+ );
1015
+ assert!(
1016
+ sent[0]
1017
+ .body
1018
+ .contains(&format!("metadata%5Bregistration_id%5D={reference}")),
1019
+ "{}",
1020
+ sent[0].body
1021
+ );
1022
+ assert!(
1023
+ sent[0].body.contains("unit_amount%5D=4200"),
1024
+ "{}",
1025
+ sent[0].body
1026
+ );
1027
+ assert!(
1028
+ sent[0].body.contains("ui_mode=embedded_page"),
1029
+ "an embedded session, not a hosted one: {}",
1030
+ sent[0].body
1031
+ );
1032
+ assert!(
1033
+ sent[0].body.contains("redirect_on_completion=never"),
1034
+ "{}",
1035
+ sent[0].body
1036
+ );
1037
+ assert!(
1038
+ !sent[0].body.contains("success_url") && !sent[0].body.contains("cancel_url"),
1039
+ "no return pages exist: {}",
1040
+ sent[0].body
1041
+ );
592
1042
 
593
1043
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
594
- assert_eq!(read["instances"][format!("Payments::Payment#{reference}")]["processor"]["value"], "stripe");
1044
+ assert_eq!(
1045
+ read["instances"][format!("Payments::Payment#{reference}")]["processor"]["value"],
1046
+ "stripe"
1047
+ );
595
1048
  }
596
1049
 
597
1050
  #[tokio::test]
@@ -603,16 +1056,32 @@ async fn a_stripe_refusal_answers_502_and_leaves_no_payment_or_registration_behi
603
1056
  t.fake.state.refuse_sessions.store(true, Ordering::SeqCst);
604
1057
 
605
1058
  let (status, body) = t.register("refused-event").await;
606
- assert_eq!((status, body["error"].as_str()), (502, Some("payments are temporarily unavailable")));
1059
+ assert_eq!(
1060
+ (status, body["error"].as_str()),
1061
+ (502, Some("payments are temporarily unavailable"))
1062
+ );
607
1063
  assert_eq!(body.get("checkout_url"), None);
608
1064
  assert_eq!(body.get("embedded_checkout"), None);
609
- assert!(!body.to_string().contains("no such price"), "Stripe's own wording stays in the log: {body}");
610
- assert_eq!(t.fake.requests_to("/v1/checkout/sessions").len(), 1, "the session was attempted");
1065
+ assert!(
1066
+ !body.to_string().contains("no such price"),
1067
+ "Stripe's own wording stays in the log: {body}"
1068
+ );
1069
+ assert_eq!(
1070
+ t.fake.requests_to("/v1/checkout/sessions").len(),
1071
+ 1,
1072
+ "the session was attempted"
1073
+ );
611
1074
 
612
1075
  // The session is opened first, so nothing was recorded for the failed try.
613
1076
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
614
- assert!(instances_for(&read, "Payments::Payment#").is_empty(), "no orphaned Payment");
615
- assert!(instances_for(&read, "CheckoutFixture::Registration#").is_empty(), "no pending Registration");
1077
+ assert!(
1078
+ instances_for(&read, "Payments::Payment#").is_empty(),
1079
+ "no orphaned Payment"
1080
+ );
1081
+ assert!(
1082
+ instances_for(&read, "CheckoutFixture::Registration#").is_empty(),
1083
+ "no pending Registration"
1084
+ );
616
1085
 
617
1086
  // Once Stripe recovers a retry registers normally.
618
1087
  t.fake.state.refuse_sessions.store(false, Ordering::SeqCst);
@@ -620,11 +1089,17 @@ async fn a_stripe_refusal_answers_502_and_leaves_no_payment_or_registration_behi
620
1089
  assert_eq!(status, 200, "{body}");
621
1090
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
622
1091
  assert_eq!(instances_for(&read, "Payments::Payment#").len(), 1);
623
- assert_eq!(instances_for(&read, "CheckoutFixture::Registration#").len(), 1);
1092
+ assert_eq!(
1093
+ instances_for(&read, "CheckoutFixture::Registration#").len(),
1094
+ 1
1095
+ );
624
1096
  }
625
1097
 
626
1098
  fn unix_secs() -> i64 {
627
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64
1099
+ std::time::SystemTime::now()
1100
+ .duration_since(std::time::UNIX_EPOCH)
1101
+ .unwrap()
1102
+ .as_secs() as i64
628
1103
  }
629
1104
 
630
1105
  #[tokio::test]
@@ -639,12 +1114,27 @@ async fn a_full_event_answers_409_and_writes_nothing_and_calls_no_stripe() {
639
1114
  let sessions = t.fake.requests_to("/v1/checkout/sessions").len();
640
1115
 
641
1116
  let (status, body) = t.register("one-seat").await;
642
- assert_eq!((status, body["error"].as_str()), (409, Some("this event is full")));
1117
+ assert_eq!(
1118
+ (status, body["error"].as_str()),
1119
+ (409, Some("this event is full"))
1120
+ );
643
1121
  assert_eq!(body.get("embedded_checkout"), None);
644
- assert_eq!(t.fake.requests_to("/v1/checkout/sessions").len(), sessions, "no session is opened for a full event");
1122
+ assert_eq!(
1123
+ t.fake.requests_to("/v1/checkout/sessions").len(),
1124
+ sessions,
1125
+ "no session is opened for a full event"
1126
+ );
645
1127
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
646
- assert_eq!(instances_for(&read, "Payments::Payment#").len(), 1, "no second Payment");
647
- assert_eq!(instances_for(&read, "CheckoutFixture::Registration#").len(), 1, "no second Registration");
1128
+ assert_eq!(
1129
+ instances_for(&read, "Payments::Payment#").len(),
1130
+ 1,
1131
+ "no second Payment"
1132
+ );
1133
+ assert_eq!(
1134
+ instances_for(&read, "CheckoutFixture::Registration#").len(),
1135
+ 1,
1136
+ "no second Registration"
1137
+ );
648
1138
  }
649
1139
 
650
1140
  #[tokio::test]
@@ -656,7 +1146,10 @@ async fn the_last_seat_goes_through_the_mock_walkthrough_and_the_next_attempt_is
656
1146
  for _ in 0..2 {
657
1147
  let (status, body) = t.register("two-seats").await;
658
1148
  assert_eq!(status, 200, "{body}");
659
- assert!(body["checkout_url"].is_string(), "the mock plan still answers a checkout_url: {body}");
1149
+ assert!(
1150
+ body["checkout_url"].is_string(),
1151
+ "the mock plan still answers a checkout_url: {body}"
1152
+ );
660
1153
  references.push(body["registration_id"].as_str().unwrap().to_string());
661
1154
  }
662
1155
  for reference in &references {
@@ -665,8 +1158,14 @@ async fn the_last_seat_goes_through_the_mock_walkthrough_and_the_next_attempt_is
665
1158
  }
666
1159
 
667
1160
  let (status, body) = t.register("two-seats").await;
668
- assert_eq!((status, body["error"].as_str()), (409, Some("this event is full")));
669
- assert!(t.fake.requests().is_empty(), "the mock walkthrough never calls Stripe");
1161
+ assert_eq!(
1162
+ (status, body["error"].as_str()),
1163
+ (409, Some("this event is full"))
1164
+ );
1165
+ assert!(
1166
+ t.fake.requests().is_empty(),
1167
+ "the mock walkthrough never calls Stripe"
1168
+ );
670
1169
  }
671
1170
 
672
1171
  #[tokio::test]
@@ -687,7 +1186,10 @@ async fn an_unpaid_checkout_holds_the_seat_until_its_session_expires() {
687
1186
  assert_eq!(t.webhook(expired(&reference)).await, 200);
688
1187
  assert_eq!(t.payment_status(&reference).await, "failed");
689
1188
  let (status, body) = t.register("held").await;
690
- assert_eq!(status, 200, "an expired checkout gives the seat back: {body}");
1189
+ assert_eq!(
1190
+ status, 200,
1191
+ "an expired checkout gives the seat back: {body}"
1192
+ );
691
1193
  }
692
1194
 
693
1195
  #[tokio::test]
@@ -697,12 +1199,21 @@ async fn a_declined_payment_frees_the_seat_and_a_paid_one_keeps_it() {
697
1199
 
698
1200
  let (_, body) = t.register("declined").await;
699
1201
  let first = body["registration_id"].as_str().unwrap().to_string();
700
- let response = web::registration_complete_route(&first, r#"{"outcome":"failed"}"#, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
1202
+ let response = web::registration_complete_route(
1203
+ &first,
1204
+ r#"{"outcome":"failed"}"#,
1205
+ Host::new(&t.client, &t.wasm, &t.config, &NeverInvoker),
1206
+ &crate::commerce_ir::fixture_payments(),
1207
+ )
1208
+ .await;
701
1209
  assert_eq!(response["statusCode"].as_u64(), Some(200));
702
1210
  assert_eq!(t.payment_status(&first).await, "failed");
703
1211
 
704
1212
  let (status, body) = t.register("declined").await;
705
- assert_eq!(status, 200, "a declined payment gives the seat back: {body}");
1213
+ assert_eq!(
1214
+ status, 200,
1215
+ "a declined payment gives the seat back: {body}"
1216
+ );
706
1217
  let second = body["registration_id"].as_str().unwrap().to_string();
707
1218
  assert_eq!(t.settle(&second).await, 200);
708
1219
 
@@ -714,12 +1225,22 @@ async fn a_declined_payment_frees_the_seat_and_a_paid_one_keeps_it() {
714
1225
  // internal dispatch does; the outcome is the kernel's own accept/refuse.
715
1226
  async fn registration_command(t: &Tenant, verb: &str, reference: &str) -> dispatch::Outcome {
716
1227
  let verb = format!("CheckoutFixture::Registration.{verb}");
717
- dispatch::handle(&t.client, &t.wasm, &verb, json!({"id": reference}), None, &t.config, &NeverInvoker).await.unwrap()
1228
+ dispatch::handle(
1229
+ Host::new(&t.client, &t.wasm, &t.config, &NeverInvoker),
1230
+ &verb,
1231
+ json!({"id": reference}),
1232
+ None,
1233
+ )
1234
+ .await
1235
+ .unwrap()
718
1236
  }
719
1237
 
720
1238
  async fn registration_status(t: &Tenant, reference: &str) -> String {
721
1239
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
722
- read["instances"][format!("CheckoutFixture::Registration#{reference}")]["status"].as_str().unwrap_or("none").to_string()
1240
+ read["instances"][format!("CheckoutFixture::Registration#{reference}")]["status"]
1241
+ .as_str()
1242
+ .unwrap_or("none")
1243
+ .to_string()
723
1244
  }
724
1245
 
725
1246
  #[tokio::test]
@@ -730,19 +1251,38 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
730
1251
  let (_, body) = t.register("archive-me").await;
731
1252
  let reference = body["registration_id"].as_str().unwrap().to_string();
732
1253
  assert_eq!(t.settle(&reference).await, 200);
733
- assert_eq!(registration_status(&t, &reference).await, "active", "a new Registration starts active");
1254
+ assert_eq!(
1255
+ registration_status(&t, &reference).await,
1256
+ "active",
1257
+ "a new Registration starts active"
1258
+ );
734
1259
  let (status, _) = t.register("archive-me").await;
735
1260
  assert_eq!(status, 409, "the paid registration holds the only seat");
736
1261
 
737
1262
  let archived = registration_command(&t, "Archive", &reference).await;
738
1263
  assert!(archived.accepted, "{:?}", archived.result);
739
1264
  assert_eq!(registration_status(&t, &reference).await, "archived");
740
- assert_eq!(t.payment_status(&reference).await, "succeeded", "archiving never touches the Payment");
1265
+ assert_eq!(
1266
+ t.payment_status(&reference).await,
1267
+ "succeeded",
1268
+ "archiving never touches the Payment"
1269
+ );
741
1270
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
742
- assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::commerce_ir::fixture_payments(), "archive-me"), Some(1));
1271
+ assert_eq!(
1272
+ web::seats_left(
1273
+ &read,
1274
+ "CheckoutFixture",
1275
+ &crate::commerce_ir::fixture_payments(),
1276
+ "archive-me"
1277
+ ),
1278
+ Some(1)
1279
+ );
743
1280
 
744
1281
  let (status, body) = t.register("archive-me").await;
745
- assert_eq!(status, 200, "the archived registration's seat is free again: {body}");
1282
+ assert_eq!(
1283
+ status, 200,
1284
+ "the archived registration's seat is free again: {body}"
1285
+ );
746
1286
  let second = body["registration_id"].as_str().unwrap().to_string();
747
1287
  assert_eq!(t.settle(&second).await, 200);
748
1288
  let (status, _) = t.register("archive-me").await;
@@ -757,7 +1297,15 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
757
1297
  2,
758
1298
  "a restored registration counts again, even past capacity: the site refuses a restore with no seat"
759
1299
  );
760
- assert_eq!(web::seats_left(&read, "CheckoutFixture", &crate::commerce_ir::fixture_payments(), "archive-me"), Some(0));
1300
+ assert_eq!(
1301
+ web::seats_left(
1302
+ &read,
1303
+ "CheckoutFixture",
1304
+ &crate::commerce_ir::fixture_payments(),
1305
+ "archive-me"
1306
+ ),
1307
+ Some(0)
1308
+ );
761
1309
  }
762
1310
 
763
1311
  #[tokio::test]
@@ -771,8 +1319,13 @@ async fn cancelling_a_paid_registration_reaches_a_state_the_fixture_declares_as_
771
1319
  let cancelled = registration_command(&t, "Cancel", &reference).await;
772
1320
  assert!(cancelled.accepted, "{:?}", cancelled.result);
773
1321
  let state = registration_status(&t, &reference).await;
774
- let declared = crate::commerce_ir::registrations_provider(&crate::commerce_ir::fixture_ir()).expect("registrations");
775
- assert!(declared.frees_seat.contains(&state), "{state} is one of {:?}", declared.frees_seat);
1322
+ let declared = crate::commerce_ir::registrations_provider(&crate::commerce_ir::fixture_ir())
1323
+ .expect("registrations");
1324
+ assert!(
1325
+ declared.frees_seat.contains(&state),
1326
+ "{state} is one of {:?}",
1327
+ declared.frees_seat
1328
+ );
776
1329
  }
777
1330
 
778
1331
  #[tokio::test]
@@ -782,9 +1335,23 @@ async fn only_an_active_registration_can_be_archived_and_only_an_archived_one_re
782
1335
  let (_, body) = t.register("guards").await;
783
1336
  let reference = body["registration_id"].as_str().unwrap().to_string();
784
1337
 
785
- assert!(!registration_command(&t, "Restore", &reference).await.accepted, "an active registration cannot be restored");
786
- assert!(registration_command(&t, "Archive", &reference).await.accepted);
787
- assert!(!registration_command(&t, "Archive", &reference).await.accepted, "an archived registration cannot be archived again");
1338
+ assert!(
1339
+ !registration_command(&t, "Restore", &reference)
1340
+ .await
1341
+ .accepted,
1342
+ "an active registration cannot be restored"
1343
+ );
1344
+ assert!(
1345
+ registration_command(&t, "Archive", &reference)
1346
+ .await
1347
+ .accepted
1348
+ );
1349
+ assert!(
1350
+ !registration_command(&t, "Archive", &reference)
1351
+ .await
1352
+ .accepted,
1353
+ "an archived registration cannot be archived again"
1354
+ );
788
1355
  assert_eq!(registration_status(&t, &reference).await, "archived");
789
1356
  }
790
1357
 
@@ -802,10 +1369,24 @@ async fn a_stripe_session_expires_about_thirty_one_minutes_after_it_is_created()
802
1369
 
803
1370
  let sent = t.fake.requests_to("/v1/checkout/sessions");
804
1371
  assert_eq!(sent.len(), 1);
805
- let expires_at: i64 = sent[0].body.split('&').find_map(|pair| pair.strip_prefix("expires_at=")).expect("an expires_at form field").parse().unwrap();
806
- let hold = crate::checkout::effective_hold_seconds(crate::commerce_ir::checkout_windows_binding().session_hold);
807
- assert!((before + hold..=after + hold).contains(&expires_at), "expires_at {expires_at} is not {hold}s after creation ({before}..{after})");
808
- assert!((30 * 60..=35 * 60).contains(&hold), "Stripe's minimum is 30 minutes, and the hold should stay close to it");
1372
+ let expires_at: i64 = sent[0]
1373
+ .body
1374
+ .split('&')
1375
+ .find_map(|pair| pair.strip_prefix("expires_at="))
1376
+ .expect("an expires_at form field")
1377
+ .parse()
1378
+ .unwrap();
1379
+ let hold = crate::checkout::effective_hold_seconds(
1380
+ crate::commerce_ir::checkout_windows_binding().session_hold,
1381
+ );
1382
+ assert!(
1383
+ (before + hold..=after + hold).contains(&expires_at),
1384
+ "expires_at {expires_at} is not {hold}s after creation ({before}..{after})"
1385
+ );
1386
+ assert!(
1387
+ (30 * 60..=35 * 60).contains(&hold),
1388
+ "Stripe's minimum is 30 minutes, and the hold should stay close to it"
1389
+ );
809
1390
  }
810
1391
 
811
1392
  #[tokio::test]
@@ -819,14 +1400,23 @@ async fn an_enabled_connection_without_a_publishable_key_is_paused_before_anythi
819
1400
  // answer must be neither the mock nor a hosted page.
820
1401
  t.platform.direct_test_publishable_key = String::new();
821
1402
  let (status, body) = t.register("nopk-event").await;
822
- assert_eq!((status, body["error"].as_str()), (503, Some("payments are temporarily unavailable")));
1403
+ assert_eq!(
1404
+ (status, body["error"].as_str()),
1405
+ (503, Some("payments are temporarily unavailable"))
1406
+ );
823
1407
  assert_eq!(body.get("checkout_url"), None);
824
1408
  assert_eq!(body.get("embedded_checkout"), None);
825
1409
 
826
1410
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
827
- assert!(instances_for(&read, "Payments::Payment#").is_empty(), "no orphaned Payment");
1411
+ assert!(
1412
+ instances_for(&read, "Payments::Payment#").is_empty(),
1413
+ "no orphaned Payment"
1414
+ );
828
1415
  assert!(instances_for(&read, "CheckoutFixture::Registration#").is_empty());
829
- assert!(t.fake.requests_to("/v1/checkout/sessions").is_empty(), "Stripe is never called");
1416
+ assert!(
1417
+ t.fake.requests_to("/v1/checkout/sessions").is_empty(),
1418
+ "Stripe is never called"
1419
+ );
830
1420
  }
831
1421
 
832
1422
  #[tokio::test]
@@ -835,12 +1425,22 @@ async fn a_paused_connection_refuses_registrations_with_a_503_and_never_falls_ba
835
1425
  t.schedule_event("paused-event").await;
836
1426
  t.connect().await;
837
1427
  t.enable().await;
838
- let (status, _) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1428
+ let (status, _) = t
1429
+ .call(
1430
+ "POST",
1431
+ "/payments/connection/disconnect",
1432
+ json!({}),
1433
+ Some(OWNER),
1434
+ )
1435
+ .await;
839
1436
  assert_eq!(status, 200);
840
1437
  assert_eq!(t.status().await, "paused");
841
1438
 
842
1439
  let (status, body) = t.register("paused-event").await;
843
- assert_eq!((status, body["error"].as_str()), (503, Some("payments are temporarily unavailable")));
1440
+ assert_eq!(
1441
+ (status, body["error"].as_str()),
1442
+ (503, Some("payments are temporarily unavailable"))
1443
+ );
844
1444
  assert_eq!(body.get("checkout_url"), None);
845
1445
 
846
1446
  // Nothing was written: no Payment, no Registration, no Stripe call.
@@ -859,7 +1459,10 @@ async fn an_enabled_connection_whose_secret_key_is_missing_is_paused_not_mock()
859
1459
 
860
1460
  t.platform.direct_test_key = String::new();
861
1461
  let (status, body) = t.register("nokey-event").await;
862
- assert_eq!((status, body["error"].as_str()), (503, Some("payments are temporarily unavailable")));
1462
+ assert_eq!(
1463
+ (status, body["error"].as_str()),
1464
+ (503, Some("payments are temporarily unavailable"))
1465
+ );
863
1466
  }
864
1467
 
865
1468
  #[tokio::test]
@@ -874,7 +1477,11 @@ async fn only_a_mock_payment_can_be_settled_by_hand() {
874
1477
  let (_, real) = t.register("hand-event").await;
875
1478
  let real_ref = real["registration_id"].as_str().unwrap().to_string();
876
1479
 
877
- assert_eq!(t.settle(&real_ref).await, 403, "a real processor's payment is settled only by its webhook");
1480
+ assert_eq!(
1481
+ t.settle(&real_ref).await,
1482
+ 403,
1483
+ "a real processor's payment is settled only by its webhook"
1484
+ );
878
1485
  assert_eq!(t.payment_status(&real_ref).await, "pending");
879
1486
  assert_eq!(t.settle(&mock_ref).await, 200);
880
1487
  assert_eq!(t.payment_status(&mock_ref).await, "succeeded");
@@ -914,16 +1521,34 @@ async fn an_event_verified_only_by_the_public_mock_secret_is_refused_for_a_real_
914
1521
  t.platform.webhook_secret = None;
915
1522
  let event = completed(&reference);
916
1523
  let payload = event.to_string();
917
- let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
1524
+ let now = std::time::SystemTime::now()
1525
+ .duration_since(std::time::UNIX_EPOCH)
1526
+ .unwrap()
1527
+ .as_secs() as i64;
918
1528
  let header = sign(web::MOCK_STRIPE_WEBHOOK_SECRET, now, &payload);
919
- let response = web::webhook_route(&payload, &header, &t.platform, &t.client, &t.wasm, &t.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
1529
+ let response = web::webhook_route(
1530
+ &payload,
1531
+ &header,
1532
+ &t.platform,
1533
+ Host::new(&t.client, &t.wasm, &t.config, &NeverInvoker),
1534
+ &crate::commerce_ir::fixture_payments(),
1535
+ )
1536
+ .await;
920
1537
  assert_eq!(response["statusCode"], 500, "{event}");
921
- assert_eq!(t.payment_status(&reference).await, "pending", "a forged completion must not settle a real payment");
1538
+ assert_eq!(
1539
+ t.payment_status(&reference).await,
1540
+ "pending",
1541
+ "a forged completion must not settle a real payment"
1542
+ );
922
1543
  }
923
1544
 
924
1545
  #[test]
925
1546
  fn checkout_plan_is_decided_from_the_connection_lifecycle() {
926
- let platform = PlatformConfig { direct_test_key: "sk_test_own".to_string(), direct_test_publishable_key: "pk_test_own".to_string(), ..test_platform() };
1547
+ let platform = PlatformConfig {
1548
+ direct_test_key: "sk_test_own".to_string(),
1549
+ direct_test_publishable_key: "pk_test_own".to_string(),
1550
+ ..test_platform()
1551
+ };
927
1552
  let with = |status: &str, processor: &str, mode: &str, account_ref: &str| Connection {
928
1553
  status: status.to_string(),
929
1554
  processor: processor.to_string(),
@@ -934,22 +1559,61 @@ fn checkout_plan_is_decided_from_the_connection_lifecycle() {
934
1559
  let own = |status: &str, mode: &str| with(status, "stripe", mode, SELF_ACCOUNT);
935
1560
  assert!(matches!(checkout_plan(None, &platform), CheckoutPlan::Mock));
936
1561
  for status in ["connected", "disconnected"] {
937
- assert!(matches!(checkout_plan(Some(&own(status, "test")), &platform), CheckoutPlan::Mock), "{status}");
1562
+ assert!(
1563
+ matches!(
1564
+ checkout_plan(Some(&own(status, "test")), &platform),
1565
+ CheckoutPlan::Mock
1566
+ ),
1567
+ "{status}"
1568
+ );
938
1569
  }
939
- assert!(matches!(checkout_plan(Some(&own("paused", "test")), &platform), CheckoutPlan::Paused));
1570
+ assert!(matches!(
1571
+ checkout_plan(Some(&own("paused", "test")), &platform),
1572
+ CheckoutPlan::Paused
1573
+ ));
940
1574
  assert!(matches!(
941
1575
  checkout_plan(Some(&own("enabled", "test")), &platform),
942
1576
  CheckoutPlan::Stripe { ref api_key, ref publishable_key } if api_key == "sk_test_own" && publishable_key == "pk_test_own"
943
1577
  ));
944
- assert!(matches!(checkout_plan(Some(&own("enabled", "live")), &platform), CheckoutPlan::Paused), "no live key configured");
945
- assert!(matches!(checkout_plan(Some(&with("enabled", "paypal", "test", SELF_ACCOUNT)), &platform), CheckoutPlan::Paused), "a processor this host cannot charge");
946
1578
  assert!(
947
- matches!(checkout_plan(Some(&with("enabled", "stripe", "test", "acct_1")), &platform), CheckoutPlan::Paused),
1579
+ matches!(
1580
+ checkout_plan(Some(&own("enabled", "live")), &platform),
1581
+ CheckoutPlan::Paused
1582
+ ),
1583
+ "no live key configured"
1584
+ );
1585
+ assert!(
1586
+ matches!(
1587
+ checkout_plan(
1588
+ Some(&with("enabled", "paypal", "test", SELF_ACCOUNT)),
1589
+ &platform
1590
+ ),
1591
+ CheckoutPlan::Paused
1592
+ ),
1593
+ "a processor this host cannot charge"
1594
+ );
1595
+ assert!(
1596
+ matches!(
1597
+ checkout_plan(
1598
+ Some(&with("enabled", "stripe", "test", "acct_1")),
1599
+ &platform
1600
+ ),
1601
+ CheckoutPlan::Paused
1602
+ ),
948
1603
  "a connection to any other account cannot be charged"
949
1604
  );
950
1605
 
951
- let secret_only = PlatformConfig { direct_test_key: "sk_test_own".to_string(), ..test_platform() };
952
- assert!(matches!(checkout_plan(Some(&own("enabled", "test")), &secret_only), CheckoutPlan::Paused), "a key without its publishable key cannot mount the form");
1606
+ let secret_only = PlatformConfig {
1607
+ direct_test_key: "sk_test_own".to_string(),
1608
+ ..test_platform()
1609
+ };
1610
+ assert!(
1611
+ matches!(
1612
+ checkout_plan(Some(&own("enabled", "test")), &secret_only),
1613
+ CheckoutPlan::Paused
1614
+ ),
1615
+ "a key without its publishable key cannot mount the form"
1616
+ );
953
1617
  assert_eq!(platform.direct_modes(), vec!["test"]);
954
1618
  assert!(secret_only.direct_modes().is_empty());
955
1619
  }
@@ -960,18 +1624,58 @@ async fn using_the_own_account_needs_an_owner_a_mode_and_configured_keys() {
960
1624
  let mut t = tenant("hecks_pay_test_direct_refused").await;
961
1625
  t.platform.direct_test_key = String::new();
962
1626
  t.platform.direct_test_publishable_key = String::new();
963
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
1627
+ let (status, body) = t
1628
+ .call(
1629
+ "POST",
1630
+ "/payments/connection/direct",
1631
+ json!({"mode": "test"}),
1632
+ Some(OWNER),
1633
+ )
1634
+ .await;
964
1635
  assert_eq!(status, 422, "{body}");
965
- assert!(body["error"].as_str().unwrap().contains("not set up for test mode"), "{body}");
1636
+ assert!(
1637
+ body["error"]
1638
+ .as_str()
1639
+ .unwrap()
1640
+ .contains("not set up for test mode"),
1641
+ "{body}"
1642
+ );
966
1643
  assert_eq!(t.status().await, "none");
967
- assert!(t.fake.requests().is_empty(), "no Stripe call without configured keys");
1644
+ assert!(
1645
+ t.fake.requests().is_empty(),
1646
+ "no Stripe call without configured keys"
1647
+ );
968
1648
 
969
1649
  let t = tenant("hecks_pay_test_direct_access").await;
970
- let (status, _) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(ADMIN)).await;
1650
+ let (status, _) = t
1651
+ .call(
1652
+ "POST",
1653
+ "/payments/connection/direct",
1654
+ json!({"mode": "test"}),
1655
+ Some(ADMIN),
1656
+ )
1657
+ .await;
971
1658
  assert_eq!(status, 403, "only an Owner may choose the account");
972
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({}), Some(OWNER)).await;
973
- assert_eq!((status, body["error"].as_str()), (400, Some("missing mode")));
974
- let (status, _) = t.call("POST", "/payments/connection/direct", json!({"mode": "live"}), Some(OWNER)).await;
1659
+ let (status, body) = t
1660
+ .call(
1661
+ "POST",
1662
+ "/payments/connection/direct",
1663
+ json!({}),
1664
+ Some(OWNER),
1665
+ )
1666
+ .await;
1667
+ assert_eq!(
1668
+ (status, body["error"].as_str()),
1669
+ (400, Some("missing mode"))
1670
+ );
1671
+ let (status, _) = t
1672
+ .call(
1673
+ "POST",
1674
+ "/payments/connection/direct",
1675
+ json!({"mode": "live"}),
1676
+ Some(OWNER),
1677
+ )
1678
+ .await;
975
1679
  assert_eq!(status, 422, "live keys are not configured");
976
1680
  assert_eq!(t.status().await, "none");
977
1681
  }
@@ -979,33 +1683,93 @@ async fn using_the_own_account_needs_an_owner_a_mode_and_configured_keys() {
979
1683
  #[tokio::test]
980
1684
  async fn using_the_own_account_records_the_reserved_ref_and_only_public_facts() {
981
1685
  let t = tenant("hecks_pay_test_direct_connect").await;
982
- let (_, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
983
- assert_eq!((body["direct_modes"].clone(), body["direct"].clone(), body["adapters"].clone()), (json!(["test"]), json!(false), json!([])));
1686
+ let (_, body) = t
1687
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
1688
+ .await;
1689
+ assert_eq!(
1690
+ (
1691
+ body["direct_modes"].clone(),
1692
+ body["direct"].clone(),
1693
+ body["adapters"].clone()
1694
+ ),
1695
+ (json!(["test"]), json!(false), json!([]))
1696
+ );
984
1697
 
985
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
1698
+ let (status, body) = t
1699
+ .call(
1700
+ "POST",
1701
+ "/payments/connection/direct",
1702
+ json!({"mode": "test"}),
1703
+ Some(OWNER),
1704
+ )
1705
+ .await;
986
1706
  assert_eq!(status, 200, "{body}");
987
1707
  assert_eq!(
988
- (body["status"].as_str(), body["processor"].as_str(), body["account_ref"].as_str(), body["mode"].as_str(), body["display_name"].as_str(), body["direct"].clone()),
989
- (Some("connected"), Some("stripe"), Some("self"), Some("test"), Some("Own Studio"), json!(true))
1708
+ (
1709
+ body["status"].as_str(),
1710
+ body["processor"].as_str(),
1711
+ body["account_ref"].as_str(),
1712
+ body["mode"].as_str(),
1713
+ body["display_name"].as_str(),
1714
+ body["direct"].clone()
1715
+ ),
1716
+ (
1717
+ Some("connected"),
1718
+ Some("stripe"),
1719
+ Some("self"),
1720
+ Some("test"),
1721
+ Some("Own Studio"),
1722
+ json!(true)
1723
+ )
990
1724
  );
991
1725
 
992
1726
  let lookups = t.fake.requests_to("/v1/account");
993
1727
  assert_eq!(lookups.len(), 1);
994
- assert_eq!(lookups[0].header("authorization"), Some(format!("Bearer {DIRECT_KEY}").as_str()), "read with the account's own key");
1728
+ assert_eq!(
1729
+ lookups[0].header("authorization"),
1730
+ Some(format!("Bearer {DIRECT_KEY}").as_str()),
1731
+ "read with the account's own key"
1732
+ );
995
1733
 
996
1734
  let dump = t.dump_database().await;
997
- assert!(!dump.contains(DIRECT_KEY) && !dump.contains(DIRECT_PUBLISHABLE_KEY), "no key is ever stored");
998
- assert!(!body.to_string().contains(DIRECT_KEY), "no key is ever shown");
1735
+ assert!(
1736
+ !dump.contains(DIRECT_KEY) && !dump.contains(DIRECT_PUBLISHABLE_KEY),
1737
+ "no key is ever stored"
1738
+ );
1739
+ assert!(
1740
+ !body.to_string().contains(DIRECT_KEY),
1741
+ "no key is ever shown"
1742
+ );
999
1743
 
1000
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
1001
- assert_eq!((status, body["error"].as_str()), (409, Some("an account is already connected — disconnect it first")));
1744
+ let (status, body) = t
1745
+ .call(
1746
+ "POST",
1747
+ "/payments/connection/direct",
1748
+ json!({"mode": "test"}),
1749
+ Some(OWNER),
1750
+ )
1751
+ .await;
1752
+ assert_eq!(
1753
+ (status, body["error"].as_str()),
1754
+ (
1755
+ 409,
1756
+ Some("an account is already connected — disconnect it first")
1757
+ )
1758
+ );
1002
1759
  }
1003
1760
 
1004
1761
  #[tokio::test]
1005
1762
  async fn the_own_account_falls_back_to_a_generic_name_when_stripe_will_not_say() {
1006
1763
  let t = tenant("hecks_pay_test_direct_name").await;
1007
1764
  t.fake.state.refuse_account.store(true, Ordering::SeqCst);
1008
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
1765
+ let (status, body) = t
1766
+ .call(
1767
+ "POST",
1768
+ "/payments/connection/direct",
1769
+ json!({"mode": "test"}),
1770
+ Some(OWNER),
1771
+ )
1772
+ .await;
1009
1773
  assert_eq!(status, 200, "{body}");
1010
1774
  assert_eq!(body["display_name"], "Your Stripe account");
1011
1775
  }
@@ -1019,7 +1783,13 @@ async fn the_own_account_charges_with_its_own_key_and_names_no_other_account() {
1019
1783
  // Connected but not enabled: still the mock, no Stripe call.
1020
1784
  let (status, body) = t.register("own-event").await;
1021
1785
  assert_eq!(status, 200, "{body}");
1022
- assert!(body["checkout_url"].as_str().unwrap().starts_with("https://site.example/pay/"), "{body}");
1786
+ assert!(
1787
+ body["checkout_url"]
1788
+ .as_str()
1789
+ .unwrap()
1790
+ .starts_with("https://site.example/pay/"),
1791
+ "{body}"
1792
+ );
1023
1793
  assert!(t.fake.requests_to("/v1/checkout/sessions").is_empty());
1024
1794
 
1025
1795
  t.enable().await;
@@ -1033,35 +1803,76 @@ async fn the_own_account_charges_with_its_own_key_and_names_no_other_account() {
1033
1803
 
1034
1804
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1035
1805
  assert_eq!(sent.len(), 1);
1036
- assert_eq!(sent[0].header("stripe-account"), None, "the business's own account is the default account");
1037
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {DIRECT_KEY}").as_str()));
1806
+ assert_eq!(
1807
+ sent[0].header("stripe-account"),
1808
+ None,
1809
+ "the business's own account is the default account"
1810
+ );
1811
+ assert_eq!(
1812
+ sent[0].header("authorization"),
1813
+ Some(format!("Bearer {DIRECT_KEY}").as_str())
1814
+ );
1038
1815
  assert_eq!(sent[0].header("stripe-version"), Some("2026-04-22.dahlia"));
1039
- assert!(sent[0].body.contains("ui_mode=embedded_page"), "{}", sent[0].body);
1816
+ assert!(
1817
+ sent[0].body.contains("ui_mode=embedded_page"),
1818
+ "{}",
1819
+ sent[0].body
1820
+ );
1040
1821
 
1041
1822
  let reference = body["registration_id"].as_str().unwrap();
1042
1823
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
1043
- assert_eq!(read["instances"][format!("Payments::Payment#{reference}")]["processor"]["value"], "stripe");
1824
+ assert_eq!(
1825
+ read["instances"][format!("Payments::Payment#{reference}")]["processor"]["value"],
1826
+ "stripe"
1827
+ );
1044
1828
  }
1045
1829
 
1046
1830
  #[tokio::test]
1047
1831
  async fn the_own_account_follows_enable_disable_pause_and_resume() {
1048
1832
  let t = tenant("hecks_pay_test_direct_lifecycle").await;
1049
- let (status, body) = t.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
1050
- assert_eq!((status, body["error"].as_str()), (409, Some("connect an account first")));
1833
+ let (status, body) = t
1834
+ .call(
1835
+ "POST",
1836
+ "/payments/connection/enable",
1837
+ json!({}),
1838
+ Some(OPERATOR),
1839
+ )
1840
+ .await;
1841
+ assert_eq!(
1842
+ (status, body["error"].as_str()),
1843
+ (409, Some("connect an account first"))
1844
+ );
1051
1845
 
1052
1846
  t.connect().await;
1053
1847
  t.enable().await;
1054
- let (status, body) = t.call("POST", "/payments/connection/disable", json!({}), Some(OPERATOR)).await;
1848
+ let (status, body) = t
1849
+ .call(
1850
+ "POST",
1851
+ "/payments/connection/disable",
1852
+ json!({}),
1853
+ Some(OPERATOR),
1854
+ )
1855
+ .await;
1055
1856
  assert_eq!((status, body["status"].as_str()), (200, Some("connected")));
1056
1857
  t.enable().await;
1057
1858
 
1058
1859
  // Disconnecting while enabled pauses registrations.
1059
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1860
+ let (status, body) = t
1861
+ .call(
1862
+ "POST",
1863
+ "/payments/connection/disconnect",
1864
+ json!({}),
1865
+ Some(OWNER),
1866
+ )
1867
+ .await;
1060
1868
  assert_eq!(status, 200, "{body}");
1061
1869
  assert_eq!(body["status"], "paused");
1062
1870
  t.schedule_event("paused-event").await;
1063
1871
  let (status, body) = t.register("paused-event").await;
1064
- assert_eq!((status, body["error"].as_str()), (503, Some("payments are temporarily unavailable")));
1872
+ assert_eq!(
1873
+ (status, body["error"].as_str()),
1874
+ (503, Some("payments are temporarily unavailable"))
1875
+ );
1065
1876
 
1066
1877
  // Choosing the account again resumes the paused connection.
1067
1878
  t.connect().await;
@@ -1070,8 +1881,18 @@ async fn the_own_account_follows_enable_disable_pause_and_resume() {
1070
1881
  // A connection that was never enabled unlinks and can be chosen again.
1071
1882
  let fresh = tenant("hecks_pay_test_direct_unlink").await;
1072
1883
  fresh.connect().await;
1073
- let (status, body) = fresh.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1074
- assert_eq!((status, body["status"].as_str()), (200, Some("disconnected")));
1884
+ let (status, body) = fresh
1885
+ .call(
1886
+ "POST",
1887
+ "/payments/connection/disconnect",
1888
+ json!({}),
1889
+ Some(OWNER),
1890
+ )
1891
+ .await;
1892
+ assert_eq!(
1893
+ (status, body["status"].as_str()),
1894
+ (200, Some("disconnected"))
1895
+ );
1075
1896
  fresh.connect().await;
1076
1897
  assert_eq!(fresh.status().await, "connected");
1077
1898
  }
@@ -1100,14 +1921,38 @@ fn assert_no_secret_in(text: &str) {
1100
1921
  #[tokio::test]
1101
1922
  async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_of_the_database() {
1102
1923
  let t = Tenant::saving("hecks_pay_test_save_ok").await;
1103
- let (_, before) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
1104
- assert_eq!((before["can_save_keys"].clone(), before["direct_modes"].clone()), (json!(true), json!([])));
1924
+ let (_, before) = t
1925
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
1926
+ .await;
1927
+ assert_eq!(
1928
+ (
1929
+ before["can_save_keys"].clone(),
1930
+ before["direct_modes"].clone()
1931
+ ),
1932
+ (json!(true), json!([]))
1933
+ );
1105
1934
 
1106
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1935
+ let (status, body) = t
1936
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
1937
+ .await;
1107
1938
  assert_eq!(status, 200, "{body}");
1108
1939
  assert_eq!(
1109
- (body["status"].as_str(), body["processor"].as_str(), body["account_ref"].as_str(), body["mode"].as_str(), body["display_name"].as_str(), body["direct"].clone()),
1110
- (Some("connected"), Some("stripe"), Some("self"), Some("test"), Some("Own Studio"), json!(true))
1940
+ (
1941
+ body["status"].as_str(),
1942
+ body["processor"].as_str(),
1943
+ body["account_ref"].as_str(),
1944
+ body["mode"].as_str(),
1945
+ body["display_name"].as_str(),
1946
+ body["direct"].clone()
1947
+ ),
1948
+ (
1949
+ Some("connected"),
1950
+ Some("stripe"),
1951
+ Some("self"),
1952
+ Some("test"),
1953
+ Some("Own Studio"),
1954
+ json!(true)
1955
+ )
1111
1956
  );
1112
1957
  assert_eq!(body["direct_modes"], json!(["test"]));
1113
1958
  assert_no_secret_in(&body.to_string());
@@ -1115,13 +1960,25 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1115
1960
  // Stripe was asked to check the key, then to deliver the three events to this site.
1116
1961
  let lookups = t.fake.requests_to("/v1/account");
1117
1962
  assert_eq!(lookups.len(), 1);
1118
- assert_eq!(lookups[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1119
- assert_eq!(lookups[0].header("stripe-version"), Some("2026-04-22.dahlia"));
1963
+ assert_eq!(
1964
+ lookups[0].header("authorization"),
1965
+ Some(format!("Bearer {SAVED_KEY}").as_str())
1966
+ );
1967
+ assert_eq!(
1968
+ lookups[0].header("stripe-version"),
1969
+ Some("2026-04-22.dahlia")
1970
+ );
1120
1971
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1121
1972
  assert_eq!(created.len(), 1);
1122
1973
  assert_eq!(created[0].method, "POST");
1123
- assert_eq!(created[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1124
- assert_eq!(created[0].header("stripe-version"), Some("2026-04-22.dahlia"));
1974
+ assert_eq!(
1975
+ created[0].header("authorization"),
1976
+ Some(format!("Bearer {SAVED_KEY}").as_str())
1977
+ );
1978
+ assert_eq!(
1979
+ created[0].header("stripe-version"),
1980
+ Some("2026-04-22.dahlia")
1981
+ );
1125
1982
  assert_eq!(created[0].header("stripe-account"), None);
1126
1983
  for expected in [
1127
1984
  "url=https%3A%2F%2Fwebhooks.example.test%2Fwebhooks%2Fstripe",
@@ -1130,7 +1987,11 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1130
1987
  "enabled_events%5B1%5D=checkout.session.expired",
1131
1988
  "enabled_events%5B2%5D=charge.refunded",
1132
1989
  ] {
1133
- assert!(created[0].body.contains(expected), "{expected} missing from {}", created[0].body);
1990
+ assert!(
1991
+ created[0].body.contains(expected),
1992
+ "{expected} missing from {}",
1993
+ created[0].body
1994
+ );
1134
1995
  }
1135
1996
 
1136
1997
  // Saved in the secret store in the agreed shape...
@@ -1139,7 +2000,16 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1139
2000
  let entry = saved["test"].as_object().unwrap();
1140
2001
  let mut names: Vec<&str> = entry.keys().map(String::as_str).collect();
1141
2002
  names.sort_unstable();
1142
- assert_eq!(names, ["publishable_key", "saved_at", "secret_key", "webhook_endpoint_id", "webhook_secret"]);
2003
+ assert_eq!(
2004
+ names,
2005
+ [
2006
+ "publishable_key",
2007
+ "saved_at",
2008
+ "secret_key",
2009
+ "webhook_endpoint_id",
2010
+ "webhook_secret"
2011
+ ]
2012
+ );
1143
2013
  assert_eq!(entry["secret_key"], SAVED_KEY);
1144
2014
  assert_eq!(entry["publishable_key"], SAVED_PUBLISHABLE_KEY);
1145
2015
  assert_eq!(entry["webhook_secret"], "whsec_SAVED_1");
@@ -1148,15 +2018,23 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1148
2018
  // ...and nowhere in the tenant's database.
1149
2019
  let dump = t.dump_database().await;
1150
2020
  assert_no_secret_in(&dump);
1151
- assert!(!dump.contains(SAVED_PUBLISHABLE_KEY), "not even the publishable key is stored there");
2021
+ assert!(
2022
+ !dump.contains(SAVED_PUBLISHABLE_KEY),
2023
+ "not even the publishable key is stored there"
2024
+ );
1152
2025
  }
1153
2026
 
1154
2027
  #[tokio::test]
1155
2028
  async fn saving_live_keys_derives_the_live_mode_from_the_keys() {
1156
2029
  let t = Tenant::saving("hecks_pay_test_save_live").await;
1157
- let (status, body) = t.save_keys(LIVE_SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2030
+ let (status, body) = t
2031
+ .save_keys(LIVE_SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, Some(OWNER))
2032
+ .await;
1158
2033
  assert_eq!(status, 200, "{body}");
1159
- assert_eq!((body["mode"].as_str(), body["direct_modes"].clone()), (Some("live"), json!(["live"])));
2034
+ assert_eq!(
2035
+ (body["mode"].as_str(), body["direct_modes"].clone()),
2036
+ (Some("live"), json!(["live"]))
2037
+ );
1160
2038
  let saved = t.saved_document();
1161
2039
  assert_eq!(saved["live"]["secret_key"], LIVE_SAVED_KEY);
1162
2040
  assert!(saved.get("test").is_none());
@@ -1166,25 +2044,45 @@ async fn saving_live_keys_derives_the_live_mode_from_the_keys() {
1166
2044
  async fn saved_keys_charge_the_business_account_and_its_saved_webhook_secret_settles_payments() {
1167
2045
  let t = Tenant::saving("hecks_pay_test_save_charge").await;
1168
2046
  t.schedule_event("saved-event").await;
1169
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2047
+ let (status, body) = t
2048
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2049
+ .await;
1170
2050
  assert_eq!(status, 200, "{body}");
1171
2051
  t.enable().await;
1172
2052
 
1173
2053
  let (status, body) = t.register("saved-event").await;
1174
2054
  assert_eq!(status, 200, "{body}");
1175
- assert_eq!(body["embedded_checkout"]["publishable_key"], SAVED_PUBLISHABLE_KEY);
2055
+ assert_eq!(
2056
+ body["embedded_checkout"]["publishable_key"],
2057
+ SAVED_PUBLISHABLE_KEY
2058
+ );
1176
2059
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1177
2060
  assert_eq!(sent.len(), 1);
1178
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1179
- assert_eq!(sent[0].header("stripe-account"), None, "the business's own account is the default account");
2061
+ assert_eq!(
2062
+ sent[0].header("authorization"),
2063
+ Some(format!("Bearer {SAVED_KEY}").as_str())
2064
+ );
2065
+ assert_eq!(
2066
+ sent[0].header("stripe-account"),
2067
+ None,
2068
+ "the business's own account is the default account"
2069
+ );
1180
2070
  assert_no_secret_in(&body.to_string());
1181
2071
 
1182
2072
  let reference = body["registration_id"].as_str().unwrap().to_string();
1183
2073
  // A delivery signed with anything else is refused.
1184
- assert_eq!(t.webhook_signed_with("whsec_someone_else", completed(&reference)).await, 400);
2074
+ assert_eq!(
2075
+ t.webhook_signed_with("whsec_someone_else", completed(&reference))
2076
+ .await,
2077
+ 400
2078
+ );
1185
2079
  assert_eq!(t.payment_status(&reference).await, "pending");
1186
2080
  // The signing secret Stripe returned when the webhook was created settles it.
1187
- assert_eq!(t.webhook_signed_with("whsec_SAVED_1", completed(&reference)).await, 200);
2081
+ assert_eq!(
2082
+ t.webhook_signed_with("whsec_SAVED_1", completed(&reference))
2083
+ .await,
2084
+ 200
2085
+ );
1188
2086
  assert_eq!(t.payment_status(&reference).await, "succeeded");
1189
2087
  // The environment's secret still works alongside it.
1190
2088
  let (_, body) = t.register("saved-event").await;
@@ -1197,22 +2095,56 @@ async fn saved_keys_charge_the_business_account_and_its_saved_webhook_secret_set
1197
2095
  async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1198
2096
  // No key store on this deploy.
1199
2097
  let t = tenant("hecks_pay_test_save_nostore").await;
1200
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1201
- assert_eq!((status, body["error"].as_str()), (422, Some("saving keys is not set up on this site")));
1202
- let (_, shown) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
2098
+ let (status, body) = t
2099
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2100
+ .await;
2101
+ assert_eq!(
2102
+ (status, body["error"].as_str()),
2103
+ (422, Some("saving keys is not set up on this site"))
2104
+ );
2105
+ let (_, shown) = t
2106
+ .call("GET", "/payments/connection", json!({}), Some(OWNER))
2107
+ .await;
1203
2108
  assert_eq!(shown["can_save_keys"], json!(false));
1204
2109
 
1205
2110
  let t = Tenant::saving("hecks_pay_test_save_refuse").await;
1206
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(ADMIN)).await.0, 403, "only an Owner may save keys");
1207
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, None).await.0, 401);
1208
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"secret_key": SAVED_KEY}), Some(OWNER)).await;
1209
- assert_eq!((status, body["error"].as_str()), (400, Some("missing publishable_key")));
2111
+ assert_eq!(
2112
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(ADMIN))
2113
+ .await
2114
+ .0,
2115
+ 403,
2116
+ "only an Owner may save keys"
2117
+ );
2118
+ assert_eq!(
2119
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, None).await.0,
2120
+ 401
2121
+ );
2122
+ let (status, body) = t
2123
+ .call(
2124
+ "POST",
2125
+ "/payments/connection/direct",
2126
+ json!({"secret_key": SAVED_KEY}),
2127
+ Some(OWNER),
2128
+ )
2129
+ .await;
2130
+ assert_eq!(
2131
+ (status, body["error"].as_str()),
2132
+ (400, Some("missing publishable_key"))
2133
+ );
1210
2134
 
1211
2135
  let cases = [
1212
2136
  ("nonsense", SAVED_PUBLISHABLE_KEY, "restricted key"),
1213
2137
  (SAVED_KEY, "nonsense", "publishable key"),
1214
- (SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, "test mode and the other is for live mode"),
1215
- (LIVE_SAVED_KEY, SAVED_PUBLISHABLE_KEY, "test mode and the other is for live mode"),
2138
+ (
2139
+ SAVED_KEY,
2140
+ LIVE_SAVED_PUBLISHABLE_KEY,
2141
+ "test mode and the other is for live mode",
2142
+ ),
2143
+ (
2144
+ LIVE_SAVED_KEY,
2145
+ SAVED_PUBLISHABLE_KEY,
2146
+ "test mode and the other is for live mode",
2147
+ ),
1216
2148
  ];
1217
2149
  for (secret, publishable, expected) in cases {
1218
2150
  let (status, body) = t.save_keys(secret, publishable, Some(OWNER)).await;
@@ -1220,18 +2152,31 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1220
2152
  assert!(body["error"].as_str().unwrap().contains(expected), "{body}");
1221
2153
  assert_no_secret_in(&body.to_string());
1222
2154
  }
1223
- assert!(t.fake.requests().is_empty(), "nothing malformed reaches Stripe");
2155
+ assert!(
2156
+ t.fake.requests().is_empty(),
2157
+ "nothing malformed reaches Stripe"
2158
+ );
1224
2159
 
1225
2160
  // Stripe does not recognise the key.
1226
2161
  t.fake.state.invalid_key.store(true, Ordering::SeqCst);
1227
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1228
- assert_eq!((status, body["error"].as_str()), (422, Some("Stripe did not accept that key.")));
1229
- assert!(t.fake.requests_to("/v1/webhook_endpoints").is_empty(), "no webhook for a key Stripe rejected");
2162
+ let (status, body) = t
2163
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2164
+ .await;
2165
+ assert_eq!(
2166
+ (status, body["error"].as_str()),
2167
+ (422, Some("Stripe did not accept that key."))
2168
+ );
2169
+ assert!(
2170
+ t.fake.requests_to("/v1/webhook_endpoints").is_empty(),
2171
+ "no webhook for a key Stripe rejected"
2172
+ );
1230
2173
  t.fake.state.invalid_key.store(false, Ordering::SeqCst);
1231
2174
 
1232
2175
  // A restricted key that may not create webhooks.
1233
2176
  t.fake.state.refuse_webhooks.store(true, Ordering::SeqCst);
1234
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2177
+ let (status, body) = t
2178
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2179
+ .await;
1235
2180
  assert_eq!(
1236
2181
  (status, body["error"].as_str()),
1237
2182
  (422, Some("That key can't create the webhook. In Stripe, give it Webhook Endpoints → Write as well and try again."))
@@ -1239,7 +2184,11 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1239
2184
  assert_no_secret_in(&body.to_string());
1240
2185
 
1241
2186
  assert_eq!(t.status().await, "none");
1242
- assert_eq!(t.saved_document(), Value::Null, "nothing was written to the store");
2187
+ assert_eq!(
2188
+ t.saved_document(),
2189
+ Value::Null,
2190
+ "nothing was written to the store"
2191
+ );
1243
2192
  assert_eq!(t.raw.as_ref().unwrap().writes.load(Ordering::SeqCst), 0);
1244
2193
  }
1245
2194
 
@@ -1247,16 +2196,29 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1247
2196
  async fn a_restricted_key_that_cannot_read_the_account_still_saves_with_a_generic_name() {
1248
2197
  let t = Tenant::saving("hecks_pay_test_save_name").await;
1249
2198
  t.fake.state.refuse_account.store(true, Ordering::SeqCst);
1250
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1251
- assert_eq!((status, body["display_name"].as_str()), (200, Some("Your Stripe account")), "{body}");
2199
+ let (status, body) = t
2200
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2201
+ .await;
2202
+ assert_eq!(
2203
+ (status, body["display_name"].as_str()),
2204
+ (200, Some("Your Stripe account")),
2205
+ "{body}"
2206
+ );
1252
2207
  }
1253
2208
 
1254
2209
  #[tokio::test]
1255
2210
  async fn saving_again_replaces_the_webhook_and_the_keys_without_disturbing_the_connection() {
1256
2211
  let t = Tenant::saving("hecks_pay_test_save_again").await;
1257
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2212
+ assert_eq!(
2213
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2214
+ .await
2215
+ .0,
2216
+ 200
2217
+ );
1258
2218
  t.enable().await;
1259
- let (status, body) = t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2219
+ let (status, body) = t
2220
+ .save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2221
+ .await;
1260
2222
  assert_eq!(status, 200, "{body}");
1261
2223
  assert_eq!(body["status"], "enabled", "the connection stays as it was");
1262
2224
  assert_no_secret_in(&body.to_string());
@@ -1264,48 +2226,114 @@ async fn saving_again_replaces_the_webhook_and_the_keys_without_disturbing_the_c
1264
2226
  // The first save's webhook was removed (with the first key), a new one made (with the second).
1265
2227
  let removed = t.fake.requests_to("/v1/webhook_endpoints/we_1");
1266
2228
  assert_eq!(removed.len(), 1);
1267
- assert_eq!((removed[0].method.as_str(), removed[0].header("authorization")), ("DELETE", Some(format!("Bearer {SAVED_KEY}").as_str())));
2229
+ assert_eq!(
2230
+ (
2231
+ removed[0].method.as_str(),
2232
+ removed[0].header("authorization")
2233
+ ),
2234
+ ("DELETE", Some(format!("Bearer {SAVED_KEY}").as_str()))
2235
+ );
1268
2236
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1269
2237
  assert_eq!(created.len(), 2);
1270
- assert_eq!(created[1].header("authorization"), Some(format!("Bearer {SAVED_KEY_TWO}").as_str()));
2238
+ assert_eq!(
2239
+ created[1].header("authorization"),
2240
+ Some(format!("Bearer {SAVED_KEY_TWO}").as_str())
2241
+ );
1271
2242
  // The new webhook exists before the old one goes, so a failed save never leaves none.
1272
2243
  let all = t.fake.requests();
1273
- let second_create = all.iter().rposition(|r| r.method == "POST" && r.path == "/v1/webhook_endpoints").unwrap();
1274
- let delete = all.iter().position(|r| r.method == "DELETE" && r.path == "/v1/webhook_endpoints/we_1").unwrap();
2244
+ let second_create = all
2245
+ .iter()
2246
+ .rposition(|r| r.method == "POST" && r.path == "/v1/webhook_endpoints")
2247
+ .unwrap();
2248
+ let delete = all
2249
+ .iter()
2250
+ .position(|r| r.method == "DELETE" && r.path == "/v1/webhook_endpoints/we_1")
2251
+ .unwrap();
1275
2252
  assert!(second_create < delete, "the new webhook was made first");
1276
2253
  let saved = t.saved_document();
1277
2254
  assert_eq!(
1278
- (saved["test"]["secret_key"].as_str(), saved["test"]["webhook_secret"].as_str(), saved["test"]["webhook_endpoint_id"].as_str()),
2255
+ (
2256
+ saved["test"]["secret_key"].as_str(),
2257
+ saved["test"]["webhook_secret"].as_str(),
2258
+ saved["test"]["webhook_endpoint_id"].as_str()
2259
+ ),
1279
2260
  (Some(SAVED_KEY_TWO), Some("whsec_SAVED_2"), Some("we_2"))
1280
2261
  );
1281
2262
 
1282
2263
  // Keys for the other mode cannot be saved over a connected account.
1283
- let (status, body) = t.save_keys(LIVE_SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1284
- assert_eq!((status, body["error"].as_str()), (409, Some("an account is already connected — disconnect it first")));
2264
+ let (status, body) = t
2265
+ .save_keys(LIVE_SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, Some(OWNER))
2266
+ .await;
2267
+ assert_eq!(
2268
+ (status, body["error"].as_str()),
2269
+ (
2270
+ 409,
2271
+ Some("an account is already connected — disconnect it first")
2272
+ )
2273
+ );
1285
2274
  }
1286
2275
 
1287
2276
  #[tokio::test]
1288
2277
  async fn a_failed_re_save_leaves_the_old_webhook_and_keys_working() {
1289
2278
  let t = Tenant::saving("hecks_pay_test_save_resave_fail").await;
1290
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2279
+ assert_eq!(
2280
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2281
+ .await
2282
+ .0,
2283
+ 200
2284
+ );
1291
2285
  t.fake.state.refuse_webhooks.store(true, Ordering::SeqCst);
1292
- let (status, body) = t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2286
+ let (status, body) = t
2287
+ .save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2288
+ .await;
1293
2289
  assert_eq!(status, 422, "{body}");
1294
- assert!(t.fake.requests().iter().all(|r| r.method != "DELETE"), "the old webhook was not removed");
2290
+ assert!(
2291
+ t.fake.requests().iter().all(|r| r.method != "DELETE"),
2292
+ "the old webhook was not removed"
2293
+ );
1295
2294
  let saved = t.saved_document();
1296
- assert_eq!((saved["test"]["secret_key"].as_str(), saved["test"]["webhook_endpoint_id"].as_str()), (Some(SAVED_KEY), Some("we_1")));
2295
+ assert_eq!(
2296
+ (
2297
+ saved["test"]["secret_key"].as_str(),
2298
+ saved["test"]["webhook_endpoint_id"].as_str()
2299
+ ),
2300
+ (Some(SAVED_KEY), Some("we_1"))
2301
+ );
1297
2302
  assert_eq!(t.status().await, "connected");
1298
2303
  }
1299
2304
 
1300
2305
  #[tokio::test]
1301
2306
  async fn disconnecting_forgets_the_keys_even_when_stripe_cannot_remove_the_webhook() {
1302
2307
  let t = Tenant::saving("hecks_pay_test_save_disc_down").await;
1303
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2308
+ assert_eq!(
2309
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2310
+ .await
2311
+ .0,
2312
+ 200
2313
+ );
1304
2314
  t.fake.state.fail_deletes.store(true, Ordering::SeqCst);
1305
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1306
- assert_eq!((status, body["status"].as_str()), (200, Some("disconnected")), "{body}");
1307
- assert_eq!(t.fake.requests_to("/v1/webhook_endpoints/we_1").len(), 1, "removal was attempted");
1308
- assert!(t.saved_document().get("test").is_none(), "the keys are gone anyway");
2315
+ let (status, body) = t
2316
+ .call(
2317
+ "POST",
2318
+ "/payments/connection/disconnect",
2319
+ json!({}),
2320
+ Some(OWNER),
2321
+ )
2322
+ .await;
2323
+ assert_eq!(
2324
+ (status, body["status"].as_str()),
2325
+ (200, Some("disconnected")),
2326
+ "{body}"
2327
+ );
2328
+ assert_eq!(
2329
+ t.fake.requests_to("/v1/webhook_endpoints/we_1").len(),
2330
+ 1,
2331
+ "removal was attempted"
2332
+ );
2333
+ assert!(
2334
+ t.saved_document().get("test").is_none(),
2335
+ "the keys are gone anyway"
2336
+ );
1309
2337
  }
1310
2338
 
1311
2339
  #[tokio::test]
@@ -1319,9 +2347,22 @@ async fn the_public_mock_secret_is_refused_whenever_a_real_credential_is_configu
1319
2347
  // Keys saved from the Payments page count too: only the saved signing secret verifies.
1320
2348
  let mut t = Tenant::saving("hecks_pay_test_mock_saved").await;
1321
2349
  t.platform.webhook_secret = None;
1322
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
1323
- assert_eq!(t.webhook_signed_with(mock, completed("ref-2")).await, 400, "the mock secret does not verify");
1324
- assert_eq!(t.webhook_signed_with("whsec_SAVED_1", completed("ref-2")).await, 200);
2350
+ assert_eq!(
2351
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2352
+ .await
2353
+ .0,
2354
+ 200
2355
+ );
2356
+ assert_eq!(
2357
+ t.webhook_signed_with(mock, completed("ref-2")).await,
2358
+ 400,
2359
+ "the mock secret does not verify"
2360
+ );
2361
+ assert_eq!(
2362
+ t.webhook_signed_with("whsec_SAVED_1", completed("ref-2"))
2363
+ .await,
2364
+ 200
2365
+ );
1325
2366
 
1326
2367
  // No saved signing secret at all: refused outright, not trusted on the mock secret.
1327
2368
  let raw = t.raw.as_ref().unwrap();
@@ -1335,24 +2376,52 @@ async fn the_public_mock_secret_is_refused_whenever_a_real_credential_is_configu
1335
2376
  #[tokio::test]
1336
2377
  async fn disconnecting_removes_the_webhook_from_stripe_and_deletes_the_saved_keys() {
1337
2378
  let t = Tenant::saving("hecks_pay_test_save_disconnect").await;
1338
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2379
+ assert_eq!(
2380
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2381
+ .await
2382
+ .0,
2383
+ 200
2384
+ );
1339
2385
  t.enable().await;
1340
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
2386
+ let (status, body) = t
2387
+ .call(
2388
+ "POST",
2389
+ "/payments/connection/disconnect",
2390
+ json!({}),
2391
+ Some(OWNER),
2392
+ )
2393
+ .await;
1341
2394
  assert_eq!(status, 200, "{body}");
1342
2395
  assert_eq!(body["status"], "paused");
1343
2396
  assert_eq!(body["direct_modes"], json!([]), "the keys are gone");
1344
2397
  let removed = t.fake.requests_to("/v1/webhook_endpoints/we_1");
1345
2398
  assert_eq!(removed.len(), 1);
1346
- assert_eq!(removed[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1347
- assert!(t.saved_document().get("test").is_none(), "{}", t.saved_document());
2399
+ assert_eq!(
2400
+ removed[0].header("authorization"),
2401
+ Some(format!("Bearer {SAVED_KEY}").as_str())
2402
+ );
2403
+ assert!(
2404
+ t.saved_document().get("test").is_none(),
2405
+ "{}",
2406
+ t.saved_document()
2407
+ );
1348
2408
 
1349
2409
  // Registrations pause rather than fall back to the mock...
1350
2410
  t.schedule_event("gone-event").await;
1351
2411
  let (status, body) = t.register("gone-event").await;
1352
- assert_eq!((status, body["error"].as_str()), (503, Some("payments are temporarily unavailable")));
2412
+ assert_eq!(
2413
+ (status, body["error"].as_str()),
2414
+ (503, Some("payments are temporarily unavailable"))
2415
+ );
1353
2416
  // ...and saving again resumes the paused connection with fresh keys.
1354
- let (status, body) = t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1355
- assert_eq!((status, body["status"].as_str()), (200, Some("enabled")), "{body}");
2417
+ let (status, body) = t
2418
+ .save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2419
+ .await;
2420
+ assert_eq!(
2421
+ (status, body["status"].as_str()),
2422
+ (200, Some("enabled")),
2423
+ "{body}"
2424
+ );
1356
2425
  assert_eq!(t.saved_document()["test"]["webhook_endpoint_id"], "we_2");
1357
2426
  }
1358
2427
 
@@ -1362,41 +2431,77 @@ async fn keys_set_in_the_environment_win_over_saved_ones() {
1362
2431
  t.platform.direct_test_key = DIRECT_KEY.to_string();
1363
2432
  t.platform.direct_test_publishable_key = DIRECT_PUBLISHABLE_KEY.to_string();
1364
2433
  t.schedule_event("env-event").await;
1365
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2434
+ let (status, body) = t
2435
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2436
+ .await;
1366
2437
  assert_eq!(status, 200, "{body}");
1367
2438
  t.enable().await;
1368
2439
 
1369
2440
  let (_, body) = t.register("env-event").await;
1370
- assert_eq!(body["embedded_checkout"]["publishable_key"], DIRECT_PUBLISHABLE_KEY);
2441
+ assert_eq!(
2442
+ body["embedded_checkout"]["publishable_key"],
2443
+ DIRECT_PUBLISHABLE_KEY
2444
+ );
1371
2445
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1372
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {DIRECT_KEY}").as_str()));
2446
+ assert_eq!(
2447
+ sent[0].header("authorization"),
2448
+ Some(format!("Bearer {DIRECT_KEY}").as_str())
2449
+ );
1373
2450
  }
1374
2451
 
1375
2452
  #[tokio::test]
1376
2453
  async fn a_store_that_cannot_be_written_saves_nothing_and_takes_the_webhook_back() {
1377
2454
  let t = Tenant::saving("hecks_pay_test_save_storefail").await;
1378
- t.raw.as_ref().unwrap().fail_writes.store(true, Ordering::SeqCst);
1379
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
2455
+ t.raw
2456
+ .as_ref()
2457
+ .unwrap()
2458
+ .fail_writes
2459
+ .store(true, Ordering::SeqCst);
2460
+ let (status, body) = t
2461
+ .save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2462
+ .await;
1380
2463
  assert_eq!(status, 500, "{body}");
1381
- assert_eq!(body["error"], "The keys could not be stored, so nothing was saved. Please try again.");
2464
+ assert_eq!(
2465
+ body["error"],
2466
+ "The keys could not be stored, so nothing was saved. Please try again."
2467
+ );
1382
2468
  assert_no_secret_in(&body.to_string());
1383
- assert_eq!(t.fake.requests_to("/v1/webhook_endpoints/we_1").len(), 1, "the webhook just made was removed again");
2469
+ assert_eq!(
2470
+ t.fake.requests_to("/v1/webhook_endpoints/we_1").len(),
2471
+ 1,
2472
+ "the webhook just made was removed again"
2473
+ );
1384
2474
  assert_eq!(t.status().await, "none");
1385
2475
  }
1386
2476
 
1387
2477
  #[tokio::test]
1388
2478
  async fn the_saved_keys_are_read_from_a_short_cache_and_a_save_refreshes_it() {
1389
2479
  let t = Tenant::saving("hecks_pay_test_save_cache").await;
1390
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2480
+ assert_eq!(
2481
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2482
+ .await
2483
+ .0,
2484
+ 200
2485
+ );
1391
2486
  let raw = t.raw.as_ref().unwrap();
1392
2487
  let reads = raw.reads.load(Ordering::SeqCst);
1393
2488
  for _ in 0..3 {
1394
- t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
2489
+ t.call("GET", "/payments/connection", json!({}), Some(OWNER))
2490
+ .await;
1395
2491
  }
1396
- assert_eq!(raw.reads.load(Ordering::SeqCst), reads, "reads are served from the cache");
2492
+ assert_eq!(
2493
+ raw.reads.load(Ordering::SeqCst),
2494
+ reads,
2495
+ "reads are served from the cache"
2496
+ );
1397
2497
 
1398
2498
  // Saving different keys shows up in the very next request, not a minute later.
1399
- assert_eq!(t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2499
+ assert_eq!(
2500
+ t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2501
+ .await
2502
+ .0,
2503
+ 200
2504
+ );
1400
2505
  assert_eq!(t.platform_now().await.direct_key("test"), SAVED_KEY_TWO);
1401
2506
  }
1402
2507
 
@@ -1405,9 +2510,16 @@ fn key_shapes_decide_the_mode_and_a_mismatch_is_refused_without_quoting_a_key()
1405
2510
  assert_eq!(keys_mode("rk_test_a", "pk_test_a"), Ok("test"));
1406
2511
  assert_eq!(keys_mode("sk_live_a", "pk_live_a"), Ok("live"));
1407
2512
  assert!(keys_mode("rk_test_a", "pk_live_a").is_err());
1408
- assert!(keys_mode("pk_test_a", "pk_test_a").is_err(), "a publishable key is not a secret key");
2513
+ assert!(
2514
+ keys_mode("pk_test_a", "pk_test_a").is_err(),
2515
+ "a publishable key is not a secret key"
2516
+ );
1409
2517
  assert!(keys_mode("rk_test_a", "sk_test_a").is_err());
1410
- for message in [keys_mode("rk_test_KEYMATERIAL", "x").unwrap_err(), keys_mode("x", "pk_test_KEYMATERIAL").unwrap_err(), keys_mode("rk_test_KEYMATERIAL", "pk_live_KEYMATERIAL").unwrap_err()] {
2518
+ for message in [
2519
+ keys_mode("rk_test_KEYMATERIAL", "x").unwrap_err(),
2520
+ keys_mode("x", "pk_test_KEYMATERIAL").unwrap_err(),
2521
+ keys_mode("rk_test_KEYMATERIAL", "pk_live_KEYMATERIAL").unwrap_err(),
2522
+ ] {
1411
2523
  assert!(!message.contains("KEYMATERIAL"), "{message}");
1412
2524
  }
1413
2525
  }
@@ -1416,27 +2528,48 @@ fn key_shapes_decide_the_mode_and_a_mismatch_is_refused_without_quoting_a_key()
1416
2528
  fn the_webhook_description_is_the_configured_text_else_the_domain_name_and_website() {
1417
2529
  assert_eq!(webhook_description(None, "Atelier"), "Atelier website");
1418
2530
  assert_eq!(webhook_description(Some(""), "Atelier"), "Atelier website");
1419
- assert_eq!(webhook_description(Some(" "), "Atelier"), "Atelier website");
1420
- assert_eq!(webhook_description(Some(" Atelier bookings "), "Atelier"), "Atelier bookings");
2531
+ assert_eq!(
2532
+ webhook_description(Some(" "), "Atelier"),
2533
+ "Atelier website"
2534
+ );
2535
+ assert_eq!(
2536
+ webhook_description(Some(" Atelier bookings "), "Atelier"),
2537
+ "Atelier bookings"
2538
+ );
1421
2539
  }
1422
2540
 
1423
2541
  #[tokio::test]
1424
2542
  async fn the_webhook_endpoint_is_created_with_the_platform_description() {
1425
2543
  let mut t = Tenant::saving("hecks_pay_test_webhook_description").await;
1426
2544
  t.platform.webhook_description = "Atelier bookings".to_string();
1427
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
2545
+ assert_eq!(
2546
+ t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER))
2547
+ .await
2548
+ .0,
2549
+ 200
2550
+ );
1428
2551
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1429
2552
  assert_eq!(created.len(), 1);
1430
- assert!(created[0].body.contains("description=Atelier+bookings"), "{}", created[0].body);
2553
+ assert!(
2554
+ created[0].body.contains("description=Atelier+bookings"),
2555
+ "{}",
2556
+ created[0].body
2557
+ );
1431
2558
  }
1432
2559
 
1433
2560
  #[test]
1434
2561
  fn purpose_tokens_only_verify_for_their_own_purpose_and_secret() {
1435
2562
  let token = auth::purpose_token("k", "one", json!({"email": "a@b.c"}), 60);
1436
- assert_eq!(auth::verify_purpose_token("k", "one", &token).unwrap()["email"], "a@b.c");
2563
+ assert_eq!(
2564
+ auth::verify_purpose_token("k", "one", &token).unwrap()["email"],
2565
+ "a@b.c"
2566
+ );
1437
2567
  assert!(auth::verify_purpose_token("k", "two", &token).is_none());
1438
2568
  assert!(auth::verify_purpose_token("other", "one", &token).is_none());
1439
- assert!(auth::verify_account_token("k", &token).is_none(), "not a session cookie");
2569
+ assert!(
2570
+ auth::verify_account_token("k", &token).is_none(),
2571
+ "not a session cookie"
2572
+ );
1440
2573
  }
1441
2574
 
1442
2575
  // ADR 0099: the processor and the modes are this adapter's own words, but the domain still has to
@@ -1444,8 +2577,20 @@ fn purpose_tokens_only_verify_for_their_own_purpose_and_secret() {
1444
2577
  #[test]
1445
2578
  fn the_adapter_words_are_ones_the_connection_domain_admits() {
1446
2579
  let ir = crate::commerce_ir::fixture_ir();
1447
- let source = ir["source_text"].as_str().expect("the fixture carries its bluebook source");
1448
- assert!(source.contains(&format!("one_of: {MODES:?}")), "ConnectionMode must admit exactly {MODES:?}");
1449
- let payments = std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../spec/fixtures/rust_host/checkout_fixture/bluebook/payments.bluebook")).expect("payments bluebook");
1450
- assert!(payments.contains(&format!("one_of: [\"mock_stripe\", \"{PROCESSOR}\"]")), "Payment's Processor must admit {PROCESSOR}");
2580
+ let source = ir["source_text"]
2581
+ .as_str()
2582
+ .expect("the fixture carries its bluebook source");
2583
+ assert!(
2584
+ source.contains(&format!("one_of: {MODES:?}")),
2585
+ "ConnectionMode must admit exactly {MODES:?}"
2586
+ );
2587
+ let payments = std::fs::read_to_string(
2588
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2589
+ .join("../../spec/fixtures/rust_host/checkout_fixture/bluebook/payments.bluebook"),
2590
+ )
2591
+ .expect("payments bluebook");
2592
+ assert!(
2593
+ payments.contains(&format!("one_of: [\"mock_stripe\", \"{PROCESSOR}\"]")),
2594
+ "Payment's Processor must admit {PROCESSOR}"
2595
+ );
1451
2596
  }