hecks 3.13.0 → 4.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 (440) 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.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  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 +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. 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);
@@ -197,12 +248,18 @@ async fn tenant(name: &str) -> Tenant {
197
248
  direct_live_publishable_key: String::new(),
198
249
  webhook_secret: Some(WEBHOOK_SECRET.to_string()),
199
250
  operators: vec![OPERATOR.to_string()],
251
+ manager_roles: roles::default_roles(),
200
252
  };
201
253
  Tenant {
202
254
  client,
203
255
  domain_ir,
204
- config: LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None },
205
- 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"),
206
263
  fake,
207
264
  platform,
208
265
  raw: None,
@@ -220,7 +277,7 @@ impl Tenant {
220
277
 
221
278
  // A tenant whose business can paste its keys into the Payments page: no
222
279
  // environment keys, only a key store (in memory).
223
- async fn saving(name: &str) -> Tenant {
280
+ async fn saving(name: &str) -> Self {
224
281
  let mut t = tenant(name).await;
225
282
  t.platform.direct_test_key = String::new();
226
283
  t.platform.direct_test_publishable_key = String::new();
@@ -231,49 +288,98 @@ impl Tenant {
231
288
  }
232
289
 
233
290
  // The Payments page's Save, as the given person.
234
- async fn save_keys(&self, secret_key: &str, publishable_key: &str, as_email: Option<&str>) -> (u64, Value) {
235
- 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
236
304
  }
237
305
 
238
306
  // What the saved-keys secret holds right now (`Null` before the first save).
239
307
  fn saved_document(&self) -> Value {
240
308
  let raw = self.raw.as_ref().expect("a tenant with a key store");
241
309
  let text = raw.document.lock().unwrap().clone();
242
- 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())
243
311
  }
244
312
 
245
313
  // One request against the payments routes; `None` means no session cookie.
246
- 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) {
247
321
  let mut cookies = HashMap::new();
248
322
  if let Some(email) = as_email {
249
- 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
+ );
250
327
  }
251
328
  let platform = self.platform_now().await;
252
- let response = route(method, path, &body.to_string(), &cookies, SESSION_SECRET, &self.domain_ir, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker)
253
- .await
254
- .expect("a payments path");
255
- (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
+ )
256
345
  }
257
346
 
258
347
  async fn status(&self) -> String {
259
348
  let read = dispatch::read(&self.client, &self.wasm).await.unwrap();
260
- 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)
261
350
  }
262
351
 
263
352
  // Owner records the business's own test-mode account as the connection.
264
353
  async fn connect(&self) {
265
- 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;
266
362
  assert_eq!(status, 200, "{body}");
267
363
  }
268
364
 
269
365
  // Every row of every table in the scratch database, as text.
270
366
  async fn dump_database(&self) -> String {
271
367
  let guard = self.client.lock().await;
272
- 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();
273
375
  let mut dump = String::new();
274
376
  for table in tables {
275
377
  let name: String = table.get(0);
276
- 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
+ {
277
383
  dump.push_str(&row.get::<_, String>(0));
278
384
  dump.push('\n');
279
385
  }
@@ -282,7 +388,14 @@ impl Tenant {
282
388
  }
283
389
 
284
390
  async fn enable(&self) {
285
- 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;
286
399
  assert_eq!(status, 200, "{body}");
287
400
  }
288
401
 
@@ -292,16 +405,33 @@ impl Tenant {
292
405
 
293
406
  async fn schedule_event_with_capacity(&self, slug: &str, capacity: i64) {
294
407
  let args = json!({"slug": {"value": slug}, "name": {"value": "Sample Studio"}, "price": {"cents": 4200}, "capacity": {"value": capacity}});
295
- 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();
296
416
  assert!(outcome.accepted, "{:?}", outcome.result);
297
417
  }
298
418
 
299
419
  // POST /registrations for `slug`; the checkout URL and registration id on success.
300
420
  async fn register(&self, slug: &str) -> (u64, Value) {
301
- 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();
302
423
  let platform = self.platform_now().await;
303
- let response = web::registrations_route(&body, &platform, &self.client, &self.wasm, &self.config, &NeverInvoker, &crate::commerce_ir::fixture_payments()).await;
304
- (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
+ )
305
435
  }
306
436
 
307
437
  async fn webhook(&self, event: Value) -> u64 {
@@ -311,23 +441,42 @@ impl Tenant {
311
441
  // A webhook delivery signed with `secret`, the way Stripe would sign it.
312
442
  async fn webhook_signed_with(&self, secret: &str, event: Value) -> u64 {
313
443
  let payload = event.to_string();
314
- 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;
315
448
  let header = sign(secret, now, &payload);
316
449
  let platform = self.platform_now().await;
317
- 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;
318
458
  response["statusCode"].as_u64().unwrap()
319
459
  }
320
460
 
321
461
  // POST /registrations/:id/complete with a successful outcome (the mock
322
462
  // walkthrough's "Pay" button).
323
463
  async fn settle(&self, reference: &str) -> u64 {
324
- 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;
325
471
  response["statusCode"].as_u64().unwrap()
326
472
  }
327
473
 
328
474
  async fn payment_status(&self, reference: &str) -> String {
329
475
  let read = dispatch::read(&self.client, &self.wasm).await.unwrap();
330
- 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()
331
480
  }
332
481
  }
333
482
 
@@ -336,7 +485,15 @@ fn sign(secret: &str, now: i64, payload: &str) -> String {
336
485
  use sha2::Sha256;
337
486
  let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
338
487
  mac.update(format!("{now}.{payload}").as_bytes());
339
- 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}")
340
497
  }
341
498
 
342
499
  fn completed(reference: &str) -> Value {
@@ -358,9 +515,16 @@ async fn every_route_refuses_a_missing_session_with_a_json_401_and_touches_nothi
358
515
  ("POST", "/payments/connection/disable"),
359
516
  ] {
360
517
  let (status, body) = t.call(method, path, json!({}), None).await;
361
- 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
+ );
362
523
  }
363
- 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
+ );
364
528
  assert_eq!(t.status().await, "none");
365
529
  }
366
530
 
@@ -368,14 +532,42 @@ async fn every_route_refuses_a_missing_session_with_a_json_401_and_touches_nothi
368
532
  async fn a_tampered_or_disabled_session_is_a_401_not_an_owner() {
369
533
  let t = tenant("hecks_pay_test_401_disabled").await;
370
534
  let mut cookies = HashMap::new();
371
- cookies.insert(auth::account_cookie_name(), "garbage.notasignature".to_string());
372
- 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();
373
551
  assert_eq!(response["statusCode"], 401);
374
552
 
375
553
  // An Owner disabled after the cookie was signed: the signature is still valid.
376
- 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;
377
562
  assert_eq!(status, 401);
378
- 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;
379
571
  assert_eq!(status, 401);
380
572
  }
381
573
 
@@ -384,112 +576,321 @@ async fn only_an_owner_may_connect_or_disconnect_and_only_the_operator_may_enabl
384
576
  let t = tenant("hecks_pay_test_roles").await;
385
577
 
386
578
  // A non-Owner admin: read refused, every owner action refused.
387
- 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;
388
582
  assert_eq!(status, 403);
389
- for path in ["/payments/connection/direct", "/payments/connection/disconnect"] {
390
- let (status, body) = t.call("POST", path, json!({"mode": "test"}), Some(ADMIN)).await;
391
- 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
+ );
392
595
  }
393
596
 
394
597
  t.connect().await;
395
598
  assert_eq!(t.status().await, "connected");
396
599
 
397
600
  // The Owner is not the operator: enable and disable are refused.
398
- for path in ["/payments/connection/enable", "/payments/connection/disable"] {
601
+ for path in [
602
+ "/payments/connection/enable",
603
+ "/payments/connection/disable",
604
+ ] {
399
605
  let (status, _) = t.call("POST", path, json!({}), Some(OWNER)).await;
400
606
  assert_eq!(status, 403, "{path}");
401
607
  }
402
608
  // The operator is not an Owner: cannot disconnect.
403
- 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;
404
617
  assert_eq!(status, 403);
405
- 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
+ );
406
623
 
407
624
  // ...but the operator may read the page state, and enable.
408
- 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;
409
628
  assert_eq!(status, 200);
410
- 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
+ );
411
633
  t.enable().await;
412
634
  assert_eq!(t.status().await, "enabled");
413
635
  }
414
636
 
637
+ #[tokio::test]
638
+ async fn a_site_that_lists_admin_as_a_payments_manager_lets_an_admin_manage() {
639
+ let mut t = tenant("hecks_pay_test_manager_roles").await;
640
+ t.platform.manager_roles = vec!["Owner".to_string(), "Admin".to_string()];
641
+
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
+ );
650
+ t.connect().await;
651
+ assert_eq!(t.status().await, "connected");
652
+ let (status, _) = t
653
+ .call(
654
+ "POST",
655
+ "/payments/connection/disconnect",
656
+ json!({}),
657
+ Some(ADMIN),
658
+ )
659
+ .await;
660
+ assert_eq!(status, 200);
661
+ assert_eq!(t.status().await, "disconnected");
662
+
663
+ // Enabling stays with the operator, and the Owner still manages.
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
+ );
672
+ // Someone signed out, or not listed, is refused with today's message.
673
+ let (status, _) = t.call("GET", "/payments/connection", json!({}), None).await;
674
+ assert_eq!(status, 401);
675
+ t.platform.manager_roles = vec!["Editor".to_string()];
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
+ );
688
+ }
689
+
415
690
  #[tokio::test]
416
691
  async fn an_admin_granted_owner_passes_the_payments_gate_and_keeps_the_admin_gate() {
417
692
  let t = tenant("hecks_pay_test_owner_bootstrap").await;
418
- let members = LineageConfig { domain: "Acme".to_string(), era: Some(1), mirrored: None };
419
- 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
+ };
420
704
 
421
705
  // An Admin is not an Owner: the payments routes refuse them.
422
- 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;
423
709
  assert_eq!(status, 403);
424
710
 
425
711
  // An Admin grants Owner to a new person; that person passes the Payments
426
712
  // Owner gate and is still admitted by the members admin gate.
427
713
  let body = json!({"email": "boss@example.com", "name": "Boss", "role": "Owner"}).to_string();
428
- 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;
429
724
  assert_eq!(response["statusCode"], 201, "{response:?}");
430
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some("boss@example.com")).await;
431
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
432
- 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;
433
748
  assert_eq!(response["statusCode"], 201, "{response:?}");
434
749
 
435
750
  // Granting Owner to an existing Admin turns them into an Owner too.
436
- assert!(auth::grant_access(&t.client, &t.wasm, &members, &t.domain_ir, ADMIN, "Owner").await.unwrap());
437
- let (status, body) = t.call("GET", "/payments/connection", json!({}), Some(ADMIN)).await;
438
- assert_eq!((status, body["can_manage"].clone()), (200, json!(true)), "{body}");
439
- 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());
440
767
  }
441
768
 
442
769
  #[tokio::test]
443
770
  async fn the_status_route_reports_not_connected_and_which_modes_have_keys() {
444
771
  let t = tenant("hecks_pay_test_status_empty").await;
445
- 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;
446
775
  assert_eq!(status, 200);
447
776
  assert_eq!(body["status"], "not_connected");
448
777
  assert_eq!(body["processor"], Value::Null);
449
- assert_eq!((body["direct_modes"].clone(), body["direct"].clone(), body["adapters"].clone()), (json!(["test"]), json!(false), json!([])));
450
- 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
+ );
451
790
 
452
791
  // With no keys at all, no mode is offered.
453
792
  let mut bare = tenant("hecks_pay_test_status_bare").await;
454
793
  bare.platform.direct_test_key = String::new();
455
- 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;
456
797
  assert_eq!(body["direct_modes"], json!([]));
457
798
  }
458
799
 
459
800
  #[tokio::test]
460
801
  async fn enable_needs_a_connection_and_follows_the_lifecycle() {
461
802
  let t = tenant("hecks_pay_test_enable").await;
462
- let (status, body) = t.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
463
- 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
+ );
464
815
 
465
816
  t.connect().await;
466
- let (status, body) = t.call("POST", "/payments/connection/disable", json!({}), Some(OPERATOR)).await;
467
- 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
+ );
468
829
 
469
830
  t.enable().await;
470
- 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;
471
834
  assert_eq!((status, body["status"].as_str()), (200, Some("enabled")));
472
- 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;
473
843
  assert_eq!(status, 422, "enable is refused when already enabled");
474
844
 
475
- 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;
476
853
  assert_eq!((status, body["status"].as_str()), (200, Some("connected")));
477
854
  }
478
855
 
479
856
  #[tokio::test]
480
857
  async fn disconnecting_a_not_yet_enabled_account_unlinks_it_and_it_can_be_reconnected() {
481
858
  let t = tenant("hecks_pay_test_disconnect").await;
482
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
483
- 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
+ );
484
871
 
485
872
  t.connect().await;
486
- 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;
487
881
  assert_eq!(status, 200, "{body}");
488
882
  assert_eq!(body["status"], "disconnected");
489
- 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
+ );
490
887
 
491
888
  t.connect().await;
492
- 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
+ );
493
894
  }
494
895
 
495
896
  #[tokio::test]
@@ -498,16 +899,34 @@ async fn disconnecting_while_payments_are_enabled_pauses_them() {
498
899
  t.connect().await;
499
900
  t.enable().await;
500
901
 
501
- 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;
502
910
  assert_eq!(status, 200, "{body}");
503
911
  assert_eq!(body["status"], "paused");
504
912
 
505
913
  // Paused is not disconnected: it cannot be disconnected again, and a new
506
914
  // connection resumes it instead of starting over.
507
- 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;
508
923
  assert_eq!(status, 409);
509
924
  t.connect().await;
510
- 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
+ );
511
930
  }
512
931
 
513
932
  #[tokio::test]
@@ -518,17 +937,35 @@ async fn checkout_stays_on_the_mock_until_payments_are_enabled() {
518
937
  // No connection at all.
519
938
  let (status, body) = t.register("mock-event").await;
520
939
  assert_eq!(status, 200, "{body}");
521
- 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
+ );
522
947
 
523
948
  // Connected but not enabled: still the mock.
524
949
  t.connect().await;
525
950
  let (status, body) = t.register("mock-event").await;
526
951
  assert_eq!(status, 200, "{body}");
527
- assert!(body["checkout_url"].as_str().unwrap().starts_with("https://site.example/pay/"), "{body}");
528
- 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
+ );
529
963
 
530
964
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
531
- 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();
532
969
  assert_eq!(processors, vec!["mock_stripe", "mock_stripe"]);
533
970
  }
534
971
 
@@ -543,7 +980,11 @@ async fn an_enabled_connection_charges_on_the_business_own_account() {
543
980
  assert_eq!(status, 200, "{body}");
544
981
  // The guest pays in a form embedded in the site: what the browser needs to
545
982
  // mount it, and no hosted page to send them to.
546
- 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
+ );
547
988
  assert_eq!(
548
989
  body["embedded_checkout"],
549
990
  json!({
@@ -556,17 +997,54 @@ async fn an_enabled_connection_charges_on_the_business_own_account() {
556
997
 
557
998
  let sent = t.fake.requests_to("/v1/checkout/sessions");
558
999
  assert_eq!(sent.len(), 1);
559
- assert_eq!(sent[0].header("stripe-account"), None, "the business's own account is the default account");
560
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {DIRECT_KEY}").as_str()), "authenticated with the account's own key");
561
- assert_eq!(sent[0].header("stripe-version"), Some("2026-04-22.dahlia"), "pinned, not the account's default");
562
- assert!(sent[0].body.contains(&format!("metadata%5Bregistration_id%5D={reference}")), "{}", sent[0].body);
563
- assert!(sent[0].body.contains("unit_amount%5D=4200"), "{}", sent[0].body);
564
- assert!(sent[0].body.contains("ui_mode=embedded_page"), "an embedded session, not a hosted one: {}", sent[0].body);
565
- assert!(sent[0].body.contains("redirect_on_completion=never"), "{}", sent[0].body);
566
- 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
+ );
567
1042
 
568
1043
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
569
- 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
+ );
570
1048
  }
571
1049
 
572
1050
  #[tokio::test]
@@ -578,16 +1056,32 @@ async fn a_stripe_refusal_answers_502_and_leaves_no_payment_or_registration_behi
578
1056
  t.fake.state.refuse_sessions.store(true, Ordering::SeqCst);
579
1057
 
580
1058
  let (status, body) = t.register("refused-event").await;
581
- 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
+ );
582
1063
  assert_eq!(body.get("checkout_url"), None);
583
1064
  assert_eq!(body.get("embedded_checkout"), None);
584
- assert!(!body.to_string().contains("no such price"), "Stripe's own wording stays in the log: {body}");
585
- 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
+ );
586
1074
 
587
1075
  // The session is opened first, so nothing was recorded for the failed try.
588
1076
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
589
- assert!(instances_for(&read, "Payments::Payment#").is_empty(), "no orphaned Payment");
590
- 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
+ );
591
1085
 
592
1086
  // Once Stripe recovers a retry registers normally.
593
1087
  t.fake.state.refuse_sessions.store(false, Ordering::SeqCst);
@@ -595,11 +1089,17 @@ async fn a_stripe_refusal_answers_502_and_leaves_no_payment_or_registration_behi
595
1089
  assert_eq!(status, 200, "{body}");
596
1090
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
597
1091
  assert_eq!(instances_for(&read, "Payments::Payment#").len(), 1);
598
- assert_eq!(instances_for(&read, "CheckoutFixture::Registration#").len(), 1);
1092
+ assert_eq!(
1093
+ instances_for(&read, "CheckoutFixture::Registration#").len(),
1094
+ 1
1095
+ );
599
1096
  }
600
1097
 
601
1098
  fn unix_secs() -> i64 {
602
- 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
603
1103
  }
604
1104
 
605
1105
  #[tokio::test]
@@ -614,12 +1114,27 @@ async fn a_full_event_answers_409_and_writes_nothing_and_calls_no_stripe() {
614
1114
  let sessions = t.fake.requests_to("/v1/checkout/sessions").len();
615
1115
 
616
1116
  let (status, body) = t.register("one-seat").await;
617
- 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
+ );
618
1121
  assert_eq!(body.get("embedded_checkout"), None);
619
- 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
+ );
620
1127
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
621
- assert_eq!(instances_for(&read, "Payments::Payment#").len(), 1, "no second Payment");
622
- 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
+ );
623
1138
  }
624
1139
 
625
1140
  #[tokio::test]
@@ -631,7 +1146,10 @@ async fn the_last_seat_goes_through_the_mock_walkthrough_and_the_next_attempt_is
631
1146
  for _ in 0..2 {
632
1147
  let (status, body) = t.register("two-seats").await;
633
1148
  assert_eq!(status, 200, "{body}");
634
- 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
+ );
635
1153
  references.push(body["registration_id"].as_str().unwrap().to_string());
636
1154
  }
637
1155
  for reference in &references {
@@ -640,8 +1158,14 @@ async fn the_last_seat_goes_through_the_mock_walkthrough_and_the_next_attempt_is
640
1158
  }
641
1159
 
642
1160
  let (status, body) = t.register("two-seats").await;
643
- assert_eq!((status, body["error"].as_str()), (409, Some("this event is full")));
644
- 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
+ );
645
1169
  }
646
1170
 
647
1171
  #[tokio::test]
@@ -662,7 +1186,10 @@ async fn an_unpaid_checkout_holds_the_seat_until_its_session_expires() {
662
1186
  assert_eq!(t.webhook(expired(&reference)).await, 200);
663
1187
  assert_eq!(t.payment_status(&reference).await, "failed");
664
1188
  let (status, body) = t.register("held").await;
665
- 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
+ );
666
1193
  }
667
1194
 
668
1195
  #[tokio::test]
@@ -672,12 +1199,21 @@ async fn a_declined_payment_frees_the_seat_and_a_paid_one_keeps_it() {
672
1199
 
673
1200
  let (_, body) = t.register("declined").await;
674
1201
  let first = body["registration_id"].as_str().unwrap().to_string();
675
- 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;
676
1209
  assert_eq!(response["statusCode"].as_u64(), Some(200));
677
1210
  assert_eq!(t.payment_status(&first).await, "failed");
678
1211
 
679
1212
  let (status, body) = t.register("declined").await;
680
- 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
+ );
681
1217
  let second = body["registration_id"].as_str().unwrap().to_string();
682
1218
  assert_eq!(t.settle(&second).await, 200);
683
1219
 
@@ -689,12 +1225,22 @@ async fn a_declined_payment_frees_the_seat_and_a_paid_one_keeps_it() {
689
1225
  // internal dispatch does; the outcome is the kernel's own accept/refuse.
690
1226
  async fn registration_command(t: &Tenant, verb: &str, reference: &str) -> dispatch::Outcome {
691
1227
  let verb = format!("CheckoutFixture::Registration.{verb}");
692
- 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()
693
1236
  }
694
1237
 
695
1238
  async fn registration_status(t: &Tenant, reference: &str) -> String {
696
1239
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
697
- 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()
698
1244
  }
699
1245
 
700
1246
  #[tokio::test]
@@ -705,19 +1251,38 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
705
1251
  let (_, body) = t.register("archive-me").await;
706
1252
  let reference = body["registration_id"].as_str().unwrap().to_string();
707
1253
  assert_eq!(t.settle(&reference).await, 200);
708
- 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
+ );
709
1259
  let (status, _) = t.register("archive-me").await;
710
1260
  assert_eq!(status, 409, "the paid registration holds the only seat");
711
1261
 
712
1262
  let archived = registration_command(&t, "Archive", &reference).await;
713
1263
  assert!(archived.accepted, "{:?}", archived.result);
714
1264
  assert_eq!(registration_status(&t, &reference).await, "archived");
715
- 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
+ );
716
1270
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
717
- 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
+ );
718
1280
 
719
1281
  let (status, body) = t.register("archive-me").await;
720
- 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
+ );
721
1286
  let second = body["registration_id"].as_str().unwrap().to_string();
722
1287
  assert_eq!(t.settle(&second).await, 200);
723
1288
  let (status, _) = t.register("archive-me").await;
@@ -732,7 +1297,15 @@ async fn archiving_a_paid_registration_frees_its_seat_and_restoring_it_takes_the
732
1297
  2,
733
1298
  "a restored registration counts again, even past capacity: the site refuses a restore with no seat"
734
1299
  );
735
- 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
+ );
736
1309
  }
737
1310
 
738
1311
  #[tokio::test]
@@ -746,8 +1319,13 @@ async fn cancelling_a_paid_registration_reaches_a_state_the_fixture_declares_as_
746
1319
  let cancelled = registration_command(&t, "Cancel", &reference).await;
747
1320
  assert!(cancelled.accepted, "{:?}", cancelled.result);
748
1321
  let state = registration_status(&t, &reference).await;
749
- let declared = crate::commerce_ir::registrations_provider(&crate::commerce_ir::fixture_ir()).expect("registrations");
750
- 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
+ );
751
1329
  }
752
1330
 
753
1331
  #[tokio::test]
@@ -757,9 +1335,23 @@ async fn only_an_active_registration_can_be_archived_and_only_an_archived_one_re
757
1335
  let (_, body) = t.register("guards").await;
758
1336
  let reference = body["registration_id"].as_str().unwrap().to_string();
759
1337
 
760
- assert!(!registration_command(&t, "Restore", &reference).await.accepted, "an active registration cannot be restored");
761
- assert!(registration_command(&t, "Archive", &reference).await.accepted);
762
- 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
+ );
763
1355
  assert_eq!(registration_status(&t, &reference).await, "archived");
764
1356
  }
765
1357
 
@@ -777,10 +1369,24 @@ async fn a_stripe_session_expires_about_thirty_one_minutes_after_it_is_created()
777
1369
 
778
1370
  let sent = t.fake.requests_to("/v1/checkout/sessions");
779
1371
  assert_eq!(sent.len(), 1);
780
- let expires_at: i64 = sent[0].body.split('&').find_map(|pair| pair.strip_prefix("expires_at=")).expect("an expires_at form field").parse().unwrap();
781
- let hold = crate::checkout::effective_hold_seconds(crate::commerce_ir::checkout_windows_binding().session_hold);
782
- assert!((before + hold..=after + hold).contains(&expires_at), "expires_at {expires_at} is not {hold}s after creation ({before}..{after})");
783
- 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
+ );
784
1390
  }
785
1391
 
786
1392
  #[tokio::test]
@@ -794,14 +1400,23 @@ async fn an_enabled_connection_without_a_publishable_key_is_paused_before_anythi
794
1400
  // answer must be neither the mock nor a hosted page.
795
1401
  t.platform.direct_test_publishable_key = String::new();
796
1402
  let (status, body) = t.register("nopk-event").await;
797
- 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
+ );
798
1407
  assert_eq!(body.get("checkout_url"), None);
799
1408
  assert_eq!(body.get("embedded_checkout"), None);
800
1409
 
801
1410
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
802
- 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
+ );
803
1415
  assert!(instances_for(&read, "CheckoutFixture::Registration#").is_empty());
804
- 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
+ );
805
1420
  }
806
1421
 
807
1422
  #[tokio::test]
@@ -810,12 +1425,22 @@ async fn a_paused_connection_refuses_registrations_with_a_503_and_never_falls_ba
810
1425
  t.schedule_event("paused-event").await;
811
1426
  t.connect().await;
812
1427
  t.enable().await;
813
- 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;
814
1436
  assert_eq!(status, 200);
815
1437
  assert_eq!(t.status().await, "paused");
816
1438
 
817
1439
  let (status, body) = t.register("paused-event").await;
818
- 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
+ );
819
1444
  assert_eq!(body.get("checkout_url"), None);
820
1445
 
821
1446
  // Nothing was written: no Payment, no Registration, no Stripe call.
@@ -834,7 +1459,10 @@ async fn an_enabled_connection_whose_secret_key_is_missing_is_paused_not_mock()
834
1459
 
835
1460
  t.platform.direct_test_key = String::new();
836
1461
  let (status, body) = t.register("nokey-event").await;
837
- 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
+ );
838
1466
  }
839
1467
 
840
1468
  #[tokio::test]
@@ -849,7 +1477,11 @@ async fn only_a_mock_payment_can_be_settled_by_hand() {
849
1477
  let (_, real) = t.register("hand-event").await;
850
1478
  let real_ref = real["registration_id"].as_str().unwrap().to_string();
851
1479
 
852
- 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
+ );
853
1485
  assert_eq!(t.payment_status(&real_ref).await, "pending");
854
1486
  assert_eq!(t.settle(&mock_ref).await, 200);
855
1487
  assert_eq!(t.payment_status(&mock_ref).await, "succeeded");
@@ -889,16 +1521,34 @@ async fn an_event_verified_only_by_the_public_mock_secret_is_refused_for_a_real_
889
1521
  t.platform.webhook_secret = None;
890
1522
  let event = completed(&reference);
891
1523
  let payload = event.to_string();
892
- 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;
893
1528
  let header = sign(web::MOCK_STRIPE_WEBHOOK_SECRET, now, &payload);
894
- 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;
895
1537
  assert_eq!(response["statusCode"], 500, "{event}");
896
- 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
+ );
897
1543
  }
898
1544
 
899
1545
  #[test]
900
1546
  fn checkout_plan_is_decided_from_the_connection_lifecycle() {
901
- 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
+ };
902
1552
  let with = |status: &str, processor: &str, mode: &str, account_ref: &str| Connection {
903
1553
  status: status.to_string(),
904
1554
  processor: processor.to_string(),
@@ -909,22 +1559,61 @@ fn checkout_plan_is_decided_from_the_connection_lifecycle() {
909
1559
  let own = |status: &str, mode: &str| with(status, "stripe", mode, SELF_ACCOUNT);
910
1560
  assert!(matches!(checkout_plan(None, &platform), CheckoutPlan::Mock));
911
1561
  for status in ["connected", "disconnected"] {
912
- 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
+ );
913
1569
  }
914
- 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
+ ));
915
1574
  assert!(matches!(
916
1575
  checkout_plan(Some(&own("enabled", "test")), &platform),
917
1576
  CheckoutPlan::Stripe { ref api_key, ref publishable_key } if api_key == "sk_test_own" && publishable_key == "pk_test_own"
918
1577
  ));
919
- assert!(matches!(checkout_plan(Some(&own("enabled", "live")), &platform), CheckoutPlan::Paused), "no live key configured");
920
- assert!(matches!(checkout_plan(Some(&with("enabled", "paypal", "test", SELF_ACCOUNT)), &platform), CheckoutPlan::Paused), "a processor this host cannot charge");
921
1578
  assert!(
922
- 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
+ ),
923
1603
  "a connection to any other account cannot be charged"
924
1604
  );
925
1605
 
926
- let secret_only = PlatformConfig { direct_test_key: "sk_test_own".to_string(), ..test_platform() };
927
- 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
+ );
928
1617
  assert_eq!(platform.direct_modes(), vec!["test"]);
929
1618
  assert!(secret_only.direct_modes().is_empty());
930
1619
  }
@@ -935,18 +1624,58 @@ async fn using_the_own_account_needs_an_owner_a_mode_and_configured_keys() {
935
1624
  let mut t = tenant("hecks_pay_test_direct_refused").await;
936
1625
  t.platform.direct_test_key = String::new();
937
1626
  t.platform.direct_test_publishable_key = String::new();
938
- 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;
939
1635
  assert_eq!(status, 422, "{body}");
940
- 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
+ );
941
1643
  assert_eq!(t.status().await, "none");
942
- 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
+ );
943
1648
 
944
1649
  let t = tenant("hecks_pay_test_direct_access").await;
945
- 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;
946
1658
  assert_eq!(status, 403, "only an Owner may choose the account");
947
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({}), Some(OWNER)).await;
948
- assert_eq!((status, body["error"].as_str()), (400, Some("missing mode")));
949
- 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;
950
1679
  assert_eq!(status, 422, "live keys are not configured");
951
1680
  assert_eq!(t.status().await, "none");
952
1681
  }
@@ -954,33 +1683,93 @@ async fn using_the_own_account_needs_an_owner_a_mode_and_configured_keys() {
954
1683
  #[tokio::test]
955
1684
  async fn using_the_own_account_records_the_reserved_ref_and_only_public_facts() {
956
1685
  let t = tenant("hecks_pay_test_direct_connect").await;
957
- let (_, body) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
958
- 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
+ );
959
1697
 
960
- 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;
961
1706
  assert_eq!(status, 200, "{body}");
962
1707
  assert_eq!(
963
- (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()),
964
- (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
+ )
965
1724
  );
966
1725
 
967
1726
  let lookups = t.fake.requests_to("/v1/account");
968
1727
  assert_eq!(lookups.len(), 1);
969
- 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
+ );
970
1733
 
971
1734
  let dump = t.dump_database().await;
972
- assert!(!dump.contains(DIRECT_KEY) && !dump.contains(DIRECT_PUBLISHABLE_KEY), "no key is ever stored");
973
- 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
+ );
974
1743
 
975
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"mode": "test"}), Some(OWNER)).await;
976
- 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
+ );
977
1759
  }
978
1760
 
979
1761
  #[tokio::test]
980
1762
  async fn the_own_account_falls_back_to_a_generic_name_when_stripe_will_not_say() {
981
1763
  let t = tenant("hecks_pay_test_direct_name").await;
982
1764
  t.fake.state.refuse_account.store(true, Ordering::SeqCst);
983
- 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;
984
1773
  assert_eq!(status, 200, "{body}");
985
1774
  assert_eq!(body["display_name"], "Your Stripe account");
986
1775
  }
@@ -994,7 +1783,13 @@ async fn the_own_account_charges_with_its_own_key_and_names_no_other_account() {
994
1783
  // Connected but not enabled: still the mock, no Stripe call.
995
1784
  let (status, body) = t.register("own-event").await;
996
1785
  assert_eq!(status, 200, "{body}");
997
- 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
+ );
998
1793
  assert!(t.fake.requests_to("/v1/checkout/sessions").is_empty());
999
1794
 
1000
1795
  t.enable().await;
@@ -1008,35 +1803,76 @@ async fn the_own_account_charges_with_its_own_key_and_names_no_other_account() {
1008
1803
 
1009
1804
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1010
1805
  assert_eq!(sent.len(), 1);
1011
- assert_eq!(sent[0].header("stripe-account"), None, "the business's own account is the default account");
1012
- 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
+ );
1013
1815
  assert_eq!(sent[0].header("stripe-version"), Some("2026-04-22.dahlia"));
1014
- 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
+ );
1015
1821
 
1016
1822
  let reference = body["registration_id"].as_str().unwrap();
1017
1823
  let read = dispatch::read(&t.client, &t.wasm).await.unwrap();
1018
- 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
+ );
1019
1828
  }
1020
1829
 
1021
1830
  #[tokio::test]
1022
1831
  async fn the_own_account_follows_enable_disable_pause_and_resume() {
1023
1832
  let t = tenant("hecks_pay_test_direct_lifecycle").await;
1024
- let (status, body) = t.call("POST", "/payments/connection/enable", json!({}), Some(OPERATOR)).await;
1025
- 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
+ );
1026
1845
 
1027
1846
  t.connect().await;
1028
1847
  t.enable().await;
1029
- 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;
1030
1856
  assert_eq!((status, body["status"].as_str()), (200, Some("connected")));
1031
1857
  t.enable().await;
1032
1858
 
1033
1859
  // Disconnecting while enabled pauses registrations.
1034
- 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;
1035
1868
  assert_eq!(status, 200, "{body}");
1036
1869
  assert_eq!(body["status"], "paused");
1037
1870
  t.schedule_event("paused-event").await;
1038
1871
  let (status, body) = t.register("paused-event").await;
1039
- 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
+ );
1040
1876
 
1041
1877
  // Choosing the account again resumes the paused connection.
1042
1878
  t.connect().await;
@@ -1045,8 +1881,18 @@ async fn the_own_account_follows_enable_disable_pause_and_resume() {
1045
1881
  // A connection that was never enabled unlinks and can be chosen again.
1046
1882
  let fresh = tenant("hecks_pay_test_direct_unlink").await;
1047
1883
  fresh.connect().await;
1048
- let (status, body) = fresh.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1049
- 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
+ );
1050
1896
  fresh.connect().await;
1051
1897
  assert_eq!(fresh.status().await, "connected");
1052
1898
  }
@@ -1075,14 +1921,38 @@ fn assert_no_secret_in(text: &str) {
1075
1921
  #[tokio::test]
1076
1922
  async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_of_the_database() {
1077
1923
  let t = Tenant::saving("hecks_pay_test_save_ok").await;
1078
- let (_, before) = t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
1079
- 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
+ );
1080
1934
 
1081
- 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;
1082
1938
  assert_eq!(status, 200, "{body}");
1083
1939
  assert_eq!(
1084
- (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()),
1085
- (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
+ )
1086
1956
  );
1087
1957
  assert_eq!(body["direct_modes"], json!(["test"]));
1088
1958
  assert_no_secret_in(&body.to_string());
@@ -1090,13 +1960,25 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1090
1960
  // Stripe was asked to check the key, then to deliver the three events to this site.
1091
1961
  let lookups = t.fake.requests_to("/v1/account");
1092
1962
  assert_eq!(lookups.len(), 1);
1093
- assert_eq!(lookups[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1094
- 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
+ );
1095
1971
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1096
1972
  assert_eq!(created.len(), 1);
1097
1973
  assert_eq!(created[0].method, "POST");
1098
- assert_eq!(created[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1099
- 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
+ );
1100
1982
  assert_eq!(created[0].header("stripe-account"), None);
1101
1983
  for expected in [
1102
1984
  "url=https%3A%2F%2Fwebhooks.example.test%2Fwebhooks%2Fstripe",
@@ -1105,7 +1987,11 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1105
1987
  "enabled_events%5B1%5D=checkout.session.expired",
1106
1988
  "enabled_events%5B2%5D=charge.refunded",
1107
1989
  ] {
1108
- 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
+ );
1109
1995
  }
1110
1996
 
1111
1997
  // Saved in the secret store in the agreed shape...
@@ -1114,7 +2000,16 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1114
2000
  let entry = saved["test"].as_object().unwrap();
1115
2001
  let mut names: Vec<&str> = entry.keys().map(String::as_str).collect();
1116
2002
  names.sort_unstable();
1117
- 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
+ );
1118
2013
  assert_eq!(entry["secret_key"], SAVED_KEY);
1119
2014
  assert_eq!(entry["publishable_key"], SAVED_PUBLISHABLE_KEY);
1120
2015
  assert_eq!(entry["webhook_secret"], "whsec_SAVED_1");
@@ -1123,15 +2018,23 @@ async fn saving_keys_checks_them_creates_the_webhook_and_keeps_every_secret_out_
1123
2018
  // ...and nowhere in the tenant's database.
1124
2019
  let dump = t.dump_database().await;
1125
2020
  assert_no_secret_in(&dump);
1126
- 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
+ );
1127
2025
  }
1128
2026
 
1129
2027
  #[tokio::test]
1130
2028
  async fn saving_live_keys_derives_the_live_mode_from_the_keys() {
1131
2029
  let t = Tenant::saving("hecks_pay_test_save_live").await;
1132
- 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;
1133
2033
  assert_eq!(status, 200, "{body}");
1134
- 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
+ );
1135
2038
  let saved = t.saved_document();
1136
2039
  assert_eq!(saved["live"]["secret_key"], LIVE_SAVED_KEY);
1137
2040
  assert!(saved.get("test").is_none());
@@ -1141,25 +2044,45 @@ async fn saving_live_keys_derives_the_live_mode_from_the_keys() {
1141
2044
  async fn saved_keys_charge_the_business_account_and_its_saved_webhook_secret_settles_payments() {
1142
2045
  let t = Tenant::saving("hecks_pay_test_save_charge").await;
1143
2046
  t.schedule_event("saved-event").await;
1144
- 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;
1145
2050
  assert_eq!(status, 200, "{body}");
1146
2051
  t.enable().await;
1147
2052
 
1148
2053
  let (status, body) = t.register("saved-event").await;
1149
2054
  assert_eq!(status, 200, "{body}");
1150
- assert_eq!(body["embedded_checkout"]["publishable_key"], SAVED_PUBLISHABLE_KEY);
2055
+ assert_eq!(
2056
+ body["embedded_checkout"]["publishable_key"],
2057
+ SAVED_PUBLISHABLE_KEY
2058
+ );
1151
2059
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1152
2060
  assert_eq!(sent.len(), 1);
1153
- assert_eq!(sent[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1154
- 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
+ );
1155
2070
  assert_no_secret_in(&body.to_string());
1156
2071
 
1157
2072
  let reference = body["registration_id"].as_str().unwrap().to_string();
1158
2073
  // A delivery signed with anything else is refused.
1159
- 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
+ );
1160
2079
  assert_eq!(t.payment_status(&reference).await, "pending");
1161
2080
  // The signing secret Stripe returned when the webhook was created settles it.
1162
- 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
+ );
1163
2086
  assert_eq!(t.payment_status(&reference).await, "succeeded");
1164
2087
  // The environment's secret still works alongside it.
1165
2088
  let (_, body) = t.register("saved-event").await;
@@ -1172,22 +2095,56 @@ async fn saved_keys_charge_the_business_account_and_its_saved_webhook_secret_set
1172
2095
  async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1173
2096
  // No key store on this deploy.
1174
2097
  let t = tenant("hecks_pay_test_save_nostore").await;
1175
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1176
- assert_eq!((status, body["error"].as_str()), (422, Some("saving keys is not set up on this site")));
1177
- 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;
1178
2108
  assert_eq!(shown["can_save_keys"], json!(false));
1179
2109
 
1180
2110
  let t = Tenant::saving("hecks_pay_test_save_refuse").await;
1181
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(ADMIN)).await.0, 403, "only an Owner may save keys");
1182
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, None).await.0, 401);
1183
- let (status, body) = t.call("POST", "/payments/connection/direct", json!({"secret_key": SAVED_KEY}), Some(OWNER)).await;
1184
- 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
+ );
1185
2134
 
1186
2135
  let cases = [
1187
2136
  ("nonsense", SAVED_PUBLISHABLE_KEY, "restricted key"),
1188
2137
  (SAVED_KEY, "nonsense", "publishable key"),
1189
- (SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, "test mode and the other is for live mode"),
1190
- (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
+ ),
1191
2148
  ];
1192
2149
  for (secret, publishable, expected) in cases {
1193
2150
  let (status, body) = t.save_keys(secret, publishable, Some(OWNER)).await;
@@ -1195,18 +2152,31 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1195
2152
  assert!(body["error"].as_str().unwrap().contains(expected), "{body}");
1196
2153
  assert_no_secret_in(&body.to_string());
1197
2154
  }
1198
- assert!(t.fake.requests().is_empty(), "nothing malformed reaches Stripe");
2155
+ assert!(
2156
+ t.fake.requests().is_empty(),
2157
+ "nothing malformed reaches Stripe"
2158
+ );
1199
2159
 
1200
2160
  // Stripe does not recognise the key.
1201
2161
  t.fake.state.invalid_key.store(true, Ordering::SeqCst);
1202
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1203
- assert_eq!((status, body["error"].as_str()), (422, Some("Stripe did not accept that key.")));
1204
- 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
+ );
1205
2173
  t.fake.state.invalid_key.store(false, Ordering::SeqCst);
1206
2174
 
1207
2175
  // A restricted key that may not create webhooks.
1208
2176
  t.fake.state.refuse_webhooks.store(true, Ordering::SeqCst);
1209
- 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;
1210
2180
  assert_eq!(
1211
2181
  (status, body["error"].as_str()),
1212
2182
  (422, Some("That key can't create the webhook. In Stripe, give it Webhook Endpoints → Write as well and try again."))
@@ -1214,7 +2184,11 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1214
2184
  assert_no_secret_in(&body.to_string());
1215
2185
 
1216
2186
  assert_eq!(t.status().await, "none");
1217
- 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
+ );
1218
2192
  assert_eq!(t.raw.as_ref().unwrap().writes.load(Ordering::SeqCst), 0);
1219
2193
  }
1220
2194
 
@@ -1222,16 +2196,29 @@ async fn saving_keys_refuses_with_plain_messages_and_saves_nothing() {
1222
2196
  async fn a_restricted_key_that_cannot_read_the_account_still_saves_with_a_generic_name() {
1223
2197
  let t = Tenant::saving("hecks_pay_test_save_name").await;
1224
2198
  t.fake.state.refuse_account.store(true, Ordering::SeqCst);
1225
- let (status, body) = t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1226
- 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
+ );
1227
2207
  }
1228
2208
 
1229
2209
  #[tokio::test]
1230
2210
  async fn saving_again_replaces_the_webhook_and_the_keys_without_disturbing_the_connection() {
1231
2211
  let t = Tenant::saving("hecks_pay_test_save_again").await;
1232
- 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
+ );
1233
2218
  t.enable().await;
1234
- 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;
1235
2222
  assert_eq!(status, 200, "{body}");
1236
2223
  assert_eq!(body["status"], "enabled", "the connection stays as it was");
1237
2224
  assert_no_secret_in(&body.to_string());
@@ -1239,48 +2226,114 @@ async fn saving_again_replaces_the_webhook_and_the_keys_without_disturbing_the_c
1239
2226
  // The first save's webhook was removed (with the first key), a new one made (with the second).
1240
2227
  let removed = t.fake.requests_to("/v1/webhook_endpoints/we_1");
1241
2228
  assert_eq!(removed.len(), 1);
1242
- 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
+ );
1243
2236
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1244
2237
  assert_eq!(created.len(), 2);
1245
- 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
+ );
1246
2242
  // The new webhook exists before the old one goes, so a failed save never leaves none.
1247
2243
  let all = t.fake.requests();
1248
- let second_create = all.iter().rposition(|r| r.method == "POST" && r.path == "/v1/webhook_endpoints").unwrap();
1249
- 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();
1250
2252
  assert!(second_create < delete, "the new webhook was made first");
1251
2253
  let saved = t.saved_document();
1252
2254
  assert_eq!(
1253
- (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
+ ),
1254
2260
  (Some(SAVED_KEY_TWO), Some("whsec_SAVED_2"), Some("we_2"))
1255
2261
  );
1256
2262
 
1257
2263
  // Keys for the other mode cannot be saved over a connected account.
1258
- let (status, body) = t.save_keys(LIVE_SAVED_KEY, LIVE_SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1259
- 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
+ );
1260
2274
  }
1261
2275
 
1262
2276
  #[tokio::test]
1263
2277
  async fn a_failed_re_save_leaves_the_old_webhook_and_keys_working() {
1264
2278
  let t = Tenant::saving("hecks_pay_test_save_resave_fail").await;
1265
- 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
+ );
1266
2285
  t.fake.state.refuse_webhooks.store(true, Ordering::SeqCst);
1267
- 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;
1268
2289
  assert_eq!(status, 422, "{body}");
1269
- 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
+ );
1270
2294
  let saved = t.saved_document();
1271
- 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
+ );
1272
2302
  assert_eq!(t.status().await, "connected");
1273
2303
  }
1274
2304
 
1275
2305
  #[tokio::test]
1276
2306
  async fn disconnecting_forgets_the_keys_even_when_stripe_cannot_remove_the_webhook() {
1277
2307
  let t = Tenant::saving("hecks_pay_test_save_disc_down").await;
1278
- 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
+ );
1279
2314
  t.fake.state.fail_deletes.store(true, Ordering::SeqCst);
1280
- let (status, body) = t.call("POST", "/payments/connection/disconnect", json!({}), Some(OWNER)).await;
1281
- assert_eq!((status, body["status"].as_str()), (200, Some("disconnected")), "{body}");
1282
- assert_eq!(t.fake.requests_to("/v1/webhook_endpoints/we_1").len(), 1, "removal was attempted");
1283
- 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
+ );
1284
2337
  }
1285
2338
 
1286
2339
  #[tokio::test]
@@ -1294,9 +2347,22 @@ async fn the_public_mock_secret_is_refused_whenever_a_real_credential_is_configu
1294
2347
  // Keys saved from the Payments page count too: only the saved signing secret verifies.
1295
2348
  let mut t = Tenant::saving("hecks_pay_test_mock_saved").await;
1296
2349
  t.platform.webhook_secret = None;
1297
- assert_eq!(t.save_keys(SAVED_KEY, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await.0, 200);
1298
- assert_eq!(t.webhook_signed_with(mock, completed("ref-2")).await, 400, "the mock secret does not verify");
1299
- 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
+ );
1300
2366
 
1301
2367
  // No saved signing secret at all: refused outright, not trusted on the mock secret.
1302
2368
  let raw = t.raw.as_ref().unwrap();
@@ -1310,24 +2376,52 @@ async fn the_public_mock_secret_is_refused_whenever_a_real_credential_is_configu
1310
2376
  #[tokio::test]
1311
2377
  async fn disconnecting_removes_the_webhook_from_stripe_and_deletes_the_saved_keys() {
1312
2378
  let t = Tenant::saving("hecks_pay_test_save_disconnect").await;
1313
- 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
+ );
1314
2385
  t.enable().await;
1315
- 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;
1316
2394
  assert_eq!(status, 200, "{body}");
1317
2395
  assert_eq!(body["status"], "paused");
1318
2396
  assert_eq!(body["direct_modes"], json!([]), "the keys are gone");
1319
2397
  let removed = t.fake.requests_to("/v1/webhook_endpoints/we_1");
1320
2398
  assert_eq!(removed.len(), 1);
1321
- assert_eq!(removed[0].header("authorization"), Some(format!("Bearer {SAVED_KEY}").as_str()));
1322
- 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
+ );
1323
2408
 
1324
2409
  // Registrations pause rather than fall back to the mock...
1325
2410
  t.schedule_event("gone-event").await;
1326
2411
  let (status, body) = t.register("gone-event").await;
1327
- 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
+ );
1328
2416
  // ...and saving again resumes the paused connection with fresh keys.
1329
- let (status, body) = t.save_keys(SAVED_KEY_TWO, SAVED_PUBLISHABLE_KEY, Some(OWNER)).await;
1330
- 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
+ );
1331
2425
  assert_eq!(t.saved_document()["test"]["webhook_endpoint_id"], "we_2");
1332
2426
  }
1333
2427
 
@@ -1337,41 +2431,77 @@ async fn keys_set_in_the_environment_win_over_saved_ones() {
1337
2431
  t.platform.direct_test_key = DIRECT_KEY.to_string();
1338
2432
  t.platform.direct_test_publishable_key = DIRECT_PUBLISHABLE_KEY.to_string();
1339
2433
  t.schedule_event("env-event").await;
1340
- 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;
1341
2437
  assert_eq!(status, 200, "{body}");
1342
2438
  t.enable().await;
1343
2439
 
1344
2440
  let (_, body) = t.register("env-event").await;
1345
- assert_eq!(body["embedded_checkout"]["publishable_key"], DIRECT_PUBLISHABLE_KEY);
2441
+ assert_eq!(
2442
+ body["embedded_checkout"]["publishable_key"],
2443
+ DIRECT_PUBLISHABLE_KEY
2444
+ );
1346
2445
  let sent = t.fake.requests_to("/v1/checkout/sessions");
1347
- 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
+ );
1348
2450
  }
1349
2451
 
1350
2452
  #[tokio::test]
1351
2453
  async fn a_store_that_cannot_be_written_saves_nothing_and_takes_the_webhook_back() {
1352
2454
  let t = Tenant::saving("hecks_pay_test_save_storefail").await;
1353
- t.raw.as_ref().unwrap().fail_writes.store(true, Ordering::SeqCst);
1354
- 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;
1355
2463
  assert_eq!(status, 500, "{body}");
1356
- 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
+ );
1357
2468
  assert_no_secret_in(&body.to_string());
1358
- 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
+ );
1359
2474
  assert_eq!(t.status().await, "none");
1360
2475
  }
1361
2476
 
1362
2477
  #[tokio::test]
1363
2478
  async fn the_saved_keys_are_read_from_a_short_cache_and_a_save_refreshes_it() {
1364
2479
  let t = Tenant::saving("hecks_pay_test_save_cache").await;
1365
- 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
+ );
1366
2486
  let raw = t.raw.as_ref().unwrap();
1367
2487
  let reads = raw.reads.load(Ordering::SeqCst);
1368
2488
  for _ in 0..3 {
1369
- t.call("GET", "/payments/connection", json!({}), Some(OWNER)).await;
2489
+ t.call("GET", "/payments/connection", json!({}), Some(OWNER))
2490
+ .await;
1370
2491
  }
1371
- 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
+ );
1372
2497
 
1373
2498
  // Saving different keys shows up in the very next request, not a minute later.
1374
- 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
+ );
1375
2505
  assert_eq!(t.platform_now().await.direct_key("test"), SAVED_KEY_TWO);
1376
2506
  }
1377
2507
 
@@ -1380,9 +2510,16 @@ fn key_shapes_decide_the_mode_and_a_mismatch_is_refused_without_quoting_a_key()
1380
2510
  assert_eq!(keys_mode("rk_test_a", "pk_test_a"), Ok("test"));
1381
2511
  assert_eq!(keys_mode("sk_live_a", "pk_live_a"), Ok("live"));
1382
2512
  assert!(keys_mode("rk_test_a", "pk_live_a").is_err());
1383
- 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
+ );
1384
2517
  assert!(keys_mode("rk_test_a", "sk_test_a").is_err());
1385
- 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
+ ] {
1386
2523
  assert!(!message.contains("KEYMATERIAL"), "{message}");
1387
2524
  }
1388
2525
  }
@@ -1391,27 +2528,48 @@ fn key_shapes_decide_the_mode_and_a_mismatch_is_refused_without_quoting_a_key()
1391
2528
  fn the_webhook_description_is_the_configured_text_else_the_domain_name_and_website() {
1392
2529
  assert_eq!(webhook_description(None, "Atelier"), "Atelier website");
1393
2530
  assert_eq!(webhook_description(Some(""), "Atelier"), "Atelier website");
1394
- assert_eq!(webhook_description(Some(" "), "Atelier"), "Atelier website");
1395
- 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
+ );
1396
2539
  }
1397
2540
 
1398
2541
  #[tokio::test]
1399
2542
  async fn the_webhook_endpoint_is_created_with_the_platform_description() {
1400
2543
  let mut t = Tenant::saving("hecks_pay_test_webhook_description").await;
1401
2544
  t.platform.webhook_description = "Atelier bookings".to_string();
1402
- 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
+ );
1403
2551
  let created = t.fake.requests_to("/v1/webhook_endpoints");
1404
2552
  assert_eq!(created.len(), 1);
1405
- 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
+ );
1406
2558
  }
1407
2559
 
1408
2560
  #[test]
1409
2561
  fn purpose_tokens_only_verify_for_their_own_purpose_and_secret() {
1410
2562
  let token = auth::purpose_token("k", "one", json!({"email": "a@b.c"}), 60);
1411
- 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
+ );
1412
2567
  assert!(auth::verify_purpose_token("k", "two", &token).is_none());
1413
2568
  assert!(auth::verify_purpose_token("other", "one", &token).is_none());
1414
- 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
+ );
1415
2573
  }
1416
2574
 
1417
2575
  // ADR 0099: the processor and the modes are this adapter's own words, but the domain still has to
@@ -1419,8 +2577,20 @@ fn purpose_tokens_only_verify_for_their_own_purpose_and_secret() {
1419
2577
  #[test]
1420
2578
  fn the_adapter_words_are_ones_the_connection_domain_admits() {
1421
2579
  let ir = crate::commerce_ir::fixture_ir();
1422
- let source = ir["source_text"].as_str().expect("the fixture carries its bluebook source");
1423
- assert!(source.contains(&format!("one_of: {MODES:?}")), "ConnectionMode must admit exactly {MODES:?}");
1424
- 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");
1425
- 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
+ );
1426
2596
  }