kiosk-test-support 0.6.1 → 0.6.28

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c7eb9fc194090087a2bd0a1b771861d165f3fea55a0fbfd832ba2d1d0037a08
4
- data.tar.gz: a862919840551f535559671b236ac98a84dc420e07dba3c706c30d105032b4a8
3
+ metadata.gz: c1a0b3e7ba79f9c1cf3f0e0ed29b2d9c7ed3deb87eed7deae9e030637d177903
4
+ data.tar.gz: 9df61c38d3daefdc760b862cdc84e071e049ee0753172dde44877585e9949161
5
5
  SHA512:
6
- metadata.gz: 49f01347f6643ef9032955d55064c8555000336efa5a507073d29e16cd41e2c12cd851407b59ac4bc74e1c4c9dd6272e0c9cbe75aada517746c4484f8d0783ad
7
- data.tar.gz: dbe42526621ccd03a3153db7d2ed4338892915d8b6cd2975478eb4079e8034c7edcbf7a083298f5cab0f3b13e8337bf8cbe3d9a9e935b13ea8cfc53dfa38a622
6
+ metadata.gz: dc68223d652f7b3363e8368f1651a98b9c9bc3b52ef6f67e46edc752267348df2269790975a21dc9f72cd3066db4ebab33569b812b43ce66562fbfb3c0782f4e
7
+ data.tar.gz: 1e1d81c8ad29a55264b2d3b4e20a56ea6a27b7b90c0eff079680396158fb863c1f67dec1db3db7fc129edbed2c8191b06fd64bf21b5bb7d6dd0f81b1657eda02
data/CHANGELOG.md CHANGED
@@ -12,6 +12,119 @@ already written is edited.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.6.28] — 2026-10-26
16
+
17
+ - 2026-10-26: Version 0.6.28, the tree cut that matches skill 0.6.28.
18
+
19
+ ## [0.6.27] — 2026-10-26
20
+
21
+ - 2026-10-26: Version 0.6.27, the tree cut that matches skill 0.6.27.
22
+ ## [0.6.26] — 2026-10-26
23
+
24
+ - 2026-10-26: Version 0.6.26, the tree cut that matches skill 0.6.26.
25
+ ## [0.6.25] — 2026-10-26
26
+
27
+ - 2026-10-26: Version 0.6.25, the tree cut that matches skill 0.6.25.
28
+ - 2026-10-26: The README DSL example seeds, then reads, and says that the assistant helpers call the default mount path `/kiosk`.
29
+
30
+ ## [0.6.24] — 2026-10-26
31
+
32
+ - 2026-10-26: Version 0.6.24, the tree cut that matches skill 0.6.24.
33
+
34
+ ## [0.6.23] — 2026-10-26
35
+
36
+ - 2026-10-26: Version 0.6.23, the tree cut that matches skill 0.6.23.
37
+
38
+ ## [0.6.22] — 2026-10-26
39
+
40
+ - 2026-10-26: Version 0.6.22, the tree cut that matches skill 0.6.22.
41
+
42
+ ## [0.6.21] — 2026-10-26
43
+
44
+ - 2026-10-26: Version 0.6.21, the tree cut that matches skill 0.6.21.
45
+
46
+ ## [0.6.20] — 2026-10-26
47
+
48
+ - 2026-10-26: Version 0.6.20, the tree cut that matches skill 0.6.20.
49
+
50
+ ## [0.6.19] — 2026-10-26
51
+
52
+ - 2026-10-26: Version 0.6.19, the tree cut that matches skill 0.6.19.
53
+
54
+ ## [0.6.18] — 2026-10-25
55
+
56
+ - 2026-10-25: Version 0.6.18, the tree cut that matches skill 0.6.18.
57
+ - 2026-10-25: The gem description and README say that the helpers solve proof-of-work tolls.
58
+
59
+ ## [0.6.17] — 2026-10-25
60
+
61
+ - 2026-10-25: Version 0.6.17, the tree cut that matches skill 0.6.17.
62
+
63
+ ## [0.6.16] — 2026-10-25
64
+
65
+ - 2026-10-25: Version 0.6.16, the tree cut that matches skill 0.6.16.
66
+ - `SeededDatabase` empties every table on the search path but Rails' own two.
67
+
68
+ ## [0.6.15] — 2026-10-25
69
+
70
+ - 2026-10-25: Version 0.6.15, the tree cut that matches skill 0.6.15.
71
+
72
+ - 2026-10-25: `Kiosk::TestHelpers::SkillPin` (Minitest and RSpec): fails when kiosk-server gives a skill that `skill_sha256` is not the digest of, and tells how to make the new digest.
73
+
74
+ ## [0.6.14] — 2026-10-25
75
+
76
+ - 2026-10-25: Version 0.6.14, the tree cut that matches skill 0.6.14.
77
+
78
+ ## [0.6.13] — 2026-10-25
79
+
80
+ - 2026-10-25: Version 0.6.13, the tree cut that matches skill 0.6.13.
81
+
82
+ ## [0.6.12] — 2026-10-24
83
+
84
+ - 2026-10-24: Version 0.6.12, the tree cut that matches skill 0.6.12.
85
+
86
+ - 2026-10-24: The KYC stand-in replaces `c.kyc_provider` only, and keeps the audience of the provider it stands in for.
87
+
88
+ ## [0.6.11] — 2026-10-24
89
+
90
+ - 2026-10-24: Version 0.6.11, the tree cut that matches skill 0.6.11.
91
+
92
+ ## [0.6.10] — 2026-10-23
93
+
94
+ - 2026-10-23: Version 0.6.10, the tree cut that matches skill 0.6.10.
95
+
96
+ ## [0.6.9] — 2026-10-23
97
+
98
+ - 2026-10-23: Version 0.6.9, the tree cut that matches skill 0.6.9.
99
+
100
+ ## [0.6.8] — 2026-10-22
101
+
102
+ - 2026-10-22: Version 0.6.8, the tree cut that matches skill 0.6.8.
103
+
104
+ ## [0.6.7] — 2026-10-22
105
+
106
+ - 2026-10-22: Version 0.6.7, the tree cut that matches skill 0.6.7.
107
+
108
+ ## [0.6.6] — 2026-10-22
109
+
110
+ - 2026-10-22: Version 0.6.6, the tree cut that matches skill 0.6.6.
111
+
112
+ ## [0.6.5] — 2026-10-22
113
+
114
+ - 2026-10-22: Version 0.6.5, the tree cut that matches skill 0.6.5.
115
+
116
+ ## [0.6.4] — 2026-10-22
117
+
118
+ - 2026-10-22: Version 0.6.4, the tree cut that matches skill 0.6.4.
119
+
120
+ ## [0.6.3] — 2026-10-22
121
+
122
+ - 2026-10-22: Version 0.6.3, the tree cut that matches skill 0.6.3.
123
+
124
+ ## [0.6.2] — 2026-10-22
125
+
126
+ - 2026-10-22: Version 0.6.2, the tree cut that matches skill 0.6.2.
127
+
15
128
  ## [0.6.1] — 2026-10-22
16
129
 
17
130
  - 2026-10-22: Version 0.6.1, the tree cut that matches skill 0.6.1.
data/README.md CHANGED
@@ -4,54 +4,57 @@ Test helpers for [Kiosk](https://kiosk.tech) origins, and the journey-test DSL o
4
4
 
5
5
  ## What it does
6
6
 
7
- - `Kiosk::TestHelpers::Assistant` — registers, pays tolls, queries, runs actions, pays and listens for events against an origin, as an AI assistant does.
8
- - `Kiosk::TestHelpers::Wire` — the same transport without a principal: any method, any path, any headers.
7
+ - `Kiosk::TestHelpers::Assistant` — registers, solves tolls, queries, runs actions, pays and listens for events against an origin, as an AI assistant does.
8
+ - `Kiosk::TestHelpers::Wire` — the same transport without an assistant's credential: any method, any path, any headers.
9
9
  - `Kiosk::StoryTest` / `Kiosk::TestHelpers::Story` — a test told as a business story by customers' assistants and the people on the operator's site.
10
10
  - `Kiosk::TestHelpers::Kyc` — stands in for the operator's KYC provider, so a test decides how an identity check ends.
11
11
  - `Kiosk::TestHelpers::StripeMock` — starts a local stripe-mock that answers a confirmed charge as paid.
12
12
  - `Kiosk::TestHelpers::LiveServer` — serves the app over HTTP from the test process and gives the test an assistant for it.
13
13
  - `Kiosk::TestHelpers::SeededDatabase` — each test starts from the seeds and leaves the database empty.
14
14
  - `Kiosk::TestHelpers::DescriptorExamples` — the examples a served `/kiosk/schema` publishes, each checked against its schema.
15
+ - `Kiosk::TestHelpers::SkillPin` — fails when an update of kiosk-server gives a skill that your `skill_sha256` is not the digest of.
15
16
 
16
17
  Carries the shared pieces of the Kiosk journey-test DSL: the `Journey` module mixed into RSpec / Minitest tests, the pluggable `executor` contract, a `NullExecutor` for self-tests, and the structured error classes that the framework-specific matchers and assertions look for.
17
18
 
18
19
  The journey DSL comes with one of the harnesses:
19
20
 
20
- > **Not on RubyGems yet** — so every `gem` line below carries `github: "kiosk-hq/kiosk"`, which is what makes it copy-pasteable today. Publication status and the canonical install are stated once, in the monorepo README's [Install](https://github.com/kiosk-hq/kiosk#install) section.
21
-
22
21
  ```ruby
23
22
  group :test do
24
- gem "kiosk-rls-rspec", github: "kiosk-hq/kiosk" # RSpec
23
+ gem "kiosk-rls-rspec" # RSpec
25
24
  # or
26
- gem "kiosk-rls-minitest", github: "kiosk-hq/kiosk" # Minitest
25
+ gem "kiosk-rls-minitest" # Minitest
27
26
  end
28
27
  ```
29
28
 
30
29
  Both harnesses pull `kiosk-test-support` as a transitive dependency.
31
30
 
32
- Your bundle brings what the helpers drive: `puma` for `LiveServer` (`rails new` writes it), and `kiosk-pow-equihash` for the solver that pays Equihash tolls. `Wire` solves no other proof of work.
31
+ Your bundle brings what the helpers drive: `puma` for `LiveServer` (`rails new` writes it), and `kiosk-pow-equihash` for the solver that solves Equihash tolls. `Wire` solves no other proof of work.
33
32
 
34
33
  ## DSL
35
34
 
35
+ Each `as_*` scope runs in a transaction that rolls back. `kiosk_seed` writes
36
+ outside the scopes, so a test reads the rows that it seeds. With an `orders`
37
+ table whose policy shows a customer only their own rows (kiosk-rls) and
38
+ `c.enforce_db_role`:
39
+
36
40
  ```ruby
37
- as_agent_of(alice) do
38
- run_action :create_order, items: ["bread"]
39
- end
41
+ kiosk_seed :orders, owner: alice, item: "bread"
40
42
 
41
- as_user(alice) do
43
+ as_agent_of(alice) do
42
44
  expect(query("select item from orders")).to contain_exactly({ item: "bread" })
43
45
  end
44
46
 
45
- as_anonymous do
46
- expect { query("select * from orders") }.to be_rls_denied
47
+ as_agent_of(bob) do
48
+ expect(query("select item from orders")).to be_empty
49
+ expect { query("insert into orders (user_id, item) values ('#{alice.id}', 'tea')") }.to be_rls_denied
47
50
  end
48
51
  ```
49
52
 
50
53
  Helpers: `as_agent_of(user, role:)`, `as_user(user, role:)`, `as_agent(name)`, `as_anonymous`, `query(sql)`, `run_query(name, **args)`, `run_action(name, **args)`, `pay_action(name, **args)`, `kiosk_seed(table, count:, owner:, **attrs)`. See the `Kiosk::TestHelpers::Journey` docstrings for full semantics.
51
54
 
52
- ## Wiring an executor
55
+ ## Configuring an executor
53
56
 
54
- The DSL is inert until you wire an executor. In production-shaped tests you wire `Kiosk::TestHelpers::InProcessExecutor`, which runs against `kiosk-server` in your Rails app:
57
+ The DSL is inert until you configure an executor. In production-shaped tests you configure `Kiosk::TestHelpers::InProcessExecutor`, which runs against `kiosk-server` in your Rails app:
55
58
 
56
59
  ```ruby
57
60
  # spec/spec_helper.rb (RSpec) or test/test_helper.rb (Minitest)
@@ -67,11 +70,11 @@ Kiosk::TestHelpers.executor = Kiosk::TestHelpers::NullExecutor.new.tap do |e|
67
70
  end
68
71
  ```
69
72
 
70
- The `NullExecutor` is the zero-dependency fallback for unit-shaped tests; production-shaped tests wire `Kiosk::TestHelpers::InProcessExecutor` (above).
73
+ The `NullExecutor` is the zero-dependency fallback for unit-shaped tests; production-shaped tests configure `Kiosk::TestHelpers::InProcessExecutor` (above).
71
74
 
72
- ## Driving the wire as an assistant
75
+ ## Driving the protocol as an assistant
73
76
 
74
- `Kiosk::TestHelpers::Assistant` registers its key as an OAuth client and gets DPoP-bound tokens, pays proof-of-work tolls, queries, runs, pays with closed AP2 mandates (SD-JWT) and listens on `<endpoint>/events`, against any origin — a test's own live server or a deployment:
77
+ `Kiosk::TestHelpers::Assistant` registers its key as an OAuth client and gets DPoP-bound tokens, solves proof-of-work tolls, queries, runs, pays with closed AP2 mandates (SD-JWT) and listens on `/kiosk/events`. It calls the `/kiosk/...` paths, so it works against an origin that mounts the engine at the default path `/kiosk` — a test's own live server or a deployment. `Person` and the events client call the same paths:
75
78
 
76
79
  ```ruby
77
80
  require "kiosk/test_helpers/assistant"
@@ -83,7 +86,7 @@ events.subscribe("kyc_verification")
83
86
  assistant.run(rider, name: "reserve", scooter_code: "SK-001")
84
87
  ```
85
88
 
86
- Every answer carries its status, parsed body and headers. `Kiosk::TestHelpers::Wire` is the same transport without a principal: any method, any path, any headers.
89
+ Every answer carries its status, parsed body and headers. `Kiosk::TestHelpers::Wire` is the same transport without an assistant's credential: any method, any path, any headers.
87
90
 
88
91
  ## Telling a business story
89
92
 
@@ -105,7 +108,7 @@ end
105
108
 
106
109
  - `a_customer(as:)` registers an assistant; `a_newcomer(as:)` registers a key and acts for no person yet; `published(path)` reads what the origin shows anyone.
107
110
  - `Customer#asks` and `#does` answer an `Answer` (`ok?`, `refused?(code)`, `detail`, `hint`, `rows`, `header`, `tolls_paid`, `solved_toll`, `next_page`); `asks(…, unpaid: true)` or `asks(…, proofs: […])` leaves the toll to the story.
108
- - `Kiosk::StoryTest#assert_refused(answer, code)` asserts the wire refused the `Answer` with `code`.
111
+ - `Kiosk::StoryTest#assert_refused(answer, code)` asserts the protocol refused the `Answer` with `code`.
109
112
  - `Customer#sets_up_payment` and `#requests_verification` run the engine's `payment_setup` and `request_kyc`, and `#hears_verification_passed` waits for the outcome of the check it asked for last; `#pays(checkout_jwt)` signs the two closed AP2 mandates for the checkout an action answered, unchanged, and pays; on `payment_setup_required` it calls `payment_setup` and pays once more.
110
113
  - `Customer#account`, `#role` and `#claims` read its credential; `#signs_back_in` and `#with_a_fresh_credential` ask the token endpoint for a new token; `#redeems(link_code)`, `#asks_to_be_linked`, `#polls` and `#collects` are the linking ceremonies.
111
114
  - `Customer#listens_for`, `#follows(*topics, subject:, since:)` and `#hears(topic, about:, on:, **data)` read the event stream, holding each heard payload to the schema the origin publishes for its topic.
@@ -113,7 +116,7 @@ end
113
116
 
114
117
  ## Identity checks in tests
115
118
 
116
- A real identity check is a human showing documents to a provider; a test cannot do that, so `Kiosk::TestHelpers::Kyc` stands in for the operator's KYC provider for the length of each test and the test says how the check ends. The engine receives the provider's callback and runs the operator's code exactly as it would in production:
119
+ A real identity check is a human showing documents to a KYC provider; a test cannot do that, so `Kiosk::TestHelpers::Kyc` stands in for the operator's KYC provider for the length of each test and the test says how the check ends. The engine receives the provider's callback and runs the operator's code exactly as it would in production:
117
120
 
118
121
  ```ruby
119
122
  require "kiosk/test_helpers/kyc"
@@ -130,13 +133,53 @@ class LicenceStory < Kiosk::StoryTest # or an RSpec group including Kiosk::Tes
130
133
  end
131
134
  ```
132
135
 
133
- `kyc_attestation(principal, **attributes)` signs an attestation an assistant submits to `POST <endpoint>/agents/kyc`. A check the human refuses sends nothing, so there is no helper for it.
136
+ `kyc_attestation(principal, **attributes)` signs an attestation an assistant submits to `POST /kiosk/agents/kyc`. A check the human refuses sends nothing, so there is no helper for it.
134
137
 
135
138
  `Kiosk::TestHelpers::StripeMock.start` fronts a local [stripe-mock](https://github.com/stripe/stripe-mock) that answers a confirmed charge as paid. stripe-mock lists a saved card for every Customer, so once `payment_setup` has mapped one, `pay` settles.
136
139
 
140
+ ## The skill pin
141
+
142
+ kiosk-server gives the URL of the skill that AI assistants read, and each kiosk-server release gives the skill of its own version. You set `skill_sha256`, the SHA-256 of that file, in `config/initializers/kiosk.rb`. This command downloads the file from kiosk.tech and calculates the digest:
143
+
144
+ ```bash
145
+ curl -fsS "$(bin/rails runner 'puts Kiosk.configuration.skill_url')" | shasum -a 256
146
+ ```
147
+
148
+ ```ruby
149
+ c.skill_sha256 = "<the 64 hexadecimal characters that the command prints>"
150
+ ```
151
+
152
+ Then add a test that names the skill version of that digest: `X.Y.Z` from `skill-vX.Y.Z.md`. Minitest:
153
+
154
+ ```ruby
155
+ require "kiosk/test_helpers/skill_pin"
156
+
157
+ class KioskSkillPinTest < ActiveSupport::TestCase
158
+ include Kiosk::TestHelpers::SkillPin::Assertions
159
+
160
+ test "skill_sha256 is the digest of the skill that kiosk-server gives" do
161
+ assert_kiosk_skill_pin hashed_version: "X.Y.Z"
162
+ end
163
+ end
164
+ ```
165
+
166
+ RSpec:
167
+
168
+ ```ruby
169
+ require "kiosk/test_helpers/skill_pin/rspec"
170
+
171
+ RSpec.describe "the skill pin" do
172
+ it "is the digest of the skill that kiosk-server gives" do
173
+ expect(Kiosk.configuration).to pin_the_served_skill(hashed_version: "X.Y.Z")
174
+ end
175
+ end
176
+ ```
177
+
178
+ After an update of kiosk-server to a release with a different skill, the test fails. Its message gives the URL of the new skill, the command that calculates its SHA-256, and the two lines to change. The test also fails when your configuration sets `skill_url`: kiosk-server gives it.
179
+
137
180
  ## Status
138
181
 
139
- Pre-v1.0 alpha. The Journey DSL surface is stable across pre-v1.0 minor bumps; the executor contract may still evolve pre-v1.0 (`Kiosk::TestHelpers::InProcessExecutor` implements it today).
182
+ Pre-v1.0 alpha: the Journey DSL and the executor contract may change between releases. `Kiosk::TestHelpers::InProcessExecutor` implements the contract.
140
183
 
141
184
  ## License
142
185
 
@@ -8,7 +8,7 @@ module Kiosk
8
8
  class StoryTest < ActiveSupport::TestCase
9
9
  include TestHelpers::Story
10
10
 
11
- # Asserts the wire refused `response` with `code`.
11
+ # Asserts the protocol refused `response` with `code`.
12
12
  def assert_refused(response, code)
13
13
  assert response.refused?(code), "expected a #{code} refusal, got #{response}"
14
14
  end
@@ -11,7 +11,7 @@ require 'kiosk/test_helpers/assistant/events'
11
11
 
12
12
  module Kiosk
13
13
  module TestHelpers
14
- # An assistant on the wire: registers its key as an OAuth client, then
14
+ # An assistant over HTTP: registers its key as an OAuth client, then
15
15
  # queries, runs and pays as the principal its token names, paying every
16
16
  # proof-of-work toll it is asked for.
17
17
  #
@@ -87,7 +87,7 @@ module Kiosk
87
87
  wire.post('/kiosk/agents/kyc', { kyc_jws: attestation_jws }, credential(principal))
88
88
  end
89
89
 
90
- # `pay_tolls: false` answers a proof-of-work toll instead of paying it.
90
+ # `pay_tolls: false` answers a proof-of-work toll instead of solving it.
91
91
  def query(principal, name:, headers: {}, pay_tolls: true, **params)
92
92
  (pay_tolls ? wire : unpaid).get("/kiosk/#{name}", params, credential(principal).merge(headers))
93
93
  end
@@ -160,7 +160,7 @@ module Kiosk
160
160
 
161
161
  def unpaid = Wire.new(base_url:)
162
162
 
163
- # The principal's token and key, which the wire signs a DPoP proof with.
163
+ # The principal's token and key, which the protocol signs a DPoP proof with.
164
164
  def credential(principal) = wire.dpop(principal.token, principal.key)
165
165
 
166
166
  def setup_required?(answer)
@@ -74,7 +74,7 @@ module Kiosk
74
74
  routed_name = found[:kiosk_verb].to_s
75
75
  return if routed_name == verb.name
76
76
 
77
- "#{verb}: #{verb.http_method} #{path} hands the wire " \
77
+ "#{verb}: #{verb.http_method} #{path} hands the engine " \
78
78
  "kiosk_verb: #{routed_name.inspect} — the path segment and the " \
79
79
  '`defaults:` are two spellings of one name and must match'
80
80
  end
@@ -45,7 +45,7 @@ module Kiosk
45
45
  @wire.call(name, kind: kind, params: params, identity: identity_for(as))
46
46
  end
47
47
 
48
- # Uses the wire's own validators, so a query's reserved `limit`/`cursor` pass as they do on the wire.
48
+ # Uses the protocol's own validators, so a query's reserved `limit`/`cursor` pass as they do in the protocol.
49
49
  def schema_errors(payload, schema:, verb:, kind:, slot:)
50
50
  if slot.to_s == 'input_schema'
51
51
  Kiosk::Server::RequestValidation.validate_arguments!(payload, input_schema: schema, verb: verb,
@@ -25,7 +25,7 @@ module Kiosk
25
25
  # How many proofs of work the assistant solved to be answered.
26
26
  def tolls_paid = @response.proofs
27
27
 
28
- # The proofs of work that pay the toll this refusal demands.
28
+ # The proofs of work that solve the toll this refusal demands.
29
29
  def solved_toll = Wire.solve(self['challenges'])
30
30
 
31
31
  # The `Link` target with `rel="next"`: the rest of a truncated list.
@@ -50,7 +50,7 @@ module Kiosk
50
50
 
51
51
  def origin = @assistant.base_url
52
52
 
53
- # Pays any proof-of-work toll asked for, unless `unpaid:` or carrying its own `proofs:`.
53
+ # Solves any proof-of-work toll asked for, unless `unpaid:` or carrying its own `proofs:`.
54
54
  def asks(query, headers: {}, unpaid: false, proofs: nil, **params)
55
55
  headers = headers.merge('Kiosk-PoW' => JSON.generate(proofs)) if proofs
56
56
  Answer.new(@assistant.query(principal, name: query.to_s, headers:, pay_tolls: !(unpaid || proofs), **params))
@@ -13,7 +13,7 @@ module Kiosk
13
13
  DEFAULT_MESSAGE = <<~MSG.strip
14
14
  Kiosk::TestHelpers has no executor configured.
15
15
 
16
- Wire one in your test helper:
16
+ Configure one in your test helper:
17
17
 
18
18
  # spec/spec_helper.rb or test/test_helper.rb
19
19
  require "kiosk/test_helpers/in_process_executor"
@@ -33,7 +33,7 @@ module Kiosk
33
33
  DEFAULT_MESSAGE = <<~MSG.strip
34
34
  Kiosk::TestHelpers::Conformance has no origin configured.
35
35
 
36
- Wire one in your test helper:
36
+ Configure one in your test helper:
37
37
 
38
38
  # spec/spec_helper.rb or test/test_helper.rb
39
39
  require "kiosk/test_helpers/conformance/in_process_origin"
@@ -15,7 +15,7 @@ module Kiosk
15
15
  module TestHelpers
16
16
  # Calls a verb of the booted app through its route, in this process, as an
17
17
  # assistant does: one linked to the principal's account, as a person links
18
- # one, and paying any proof-of-work toll. A caller with no identity sends no
18
+ # one, and solving any proof-of-work toll. A caller with no identity sends no
19
19
  # token.
20
20
  class InProcessWire
21
21
  # `app` defaults to the Rails application, middleware included.
@@ -23,7 +23,7 @@ module Kiosk
23
23
  @app = app
24
24
  end
25
25
 
26
- # The verb's answer, or the wire error its refusal names.
26
+ # The verb's answer, or the Kiosk error its refusal names.
27
27
  def call(name, kind:, params:, identity:)
28
28
  credential = credential_for(identity) if identity
29
29
  response = request(name, kind, params, credential)
@@ -63,7 +63,7 @@ module Kiosk
63
63
  Wire::DpopCredential.new(token:, key:)
64
64
  end
65
65
 
66
- # Registers the key as a client, and pays the registration toll if the origin asks for one.
66
+ # Registers the key as a client, and solves the registration toll if the origin asks for one.
67
67
  def register(key)
68
68
  metadata = { 'jwks' => { 'keys' => [Kiosk::Server::AssistantKey.to_jwk(key)] } }
69
69
  Kiosk::Server::ClientRegistration.call(metadata)
@@ -17,21 +17,23 @@ module Kiosk
17
17
  # rider.requests_verification
18
18
  # the_verification_service_confirms(rider, age_over_18: true)
19
19
  module Kyc
20
- ISSUER = 'https://kyc.test.invalid'
21
- SETTINGS = %i[kyc_provider kyc_issuer kyc_public_key].freeze
20
+ ISSUER = 'https://kyc.test.invalid'
22
21
 
23
- # Opens checks without a network and signs attestations for them.
22
+ # Opens checks without a network and signs attestations for them, for
23
+ # the audience of the provider it stands in for.
24
24
  class Provider < Kiosk::KycProviders::Base
25
25
  Check = Data.define(:subject, :audience, :nonce, :callback_url)
26
26
 
27
27
  def self.key = @key ||= OpenSSL::PKey.generate_key('ED25519')
28
28
 
29
- def initialize
30
- super
29
+ def initialize(audience:)
30
+ super(issuer: ISSUER, public_key: self.class.key, audience:)
31
31
  @checks = {}
32
32
  end
33
33
 
34
- def open_verification(subject:, audience:, callback_url:, **)
34
+ def opens_verifications? = true
35
+
36
+ def open_verification(subject:, callback_url:, **)
35
37
  request_id = SecureRandom.uuid
36
38
  @checks[request_id] = Check.new(subject:, audience:, nonce: SecureRandom.hex(16), callback_url:)
37
39
  { request_id:, nonce: @checks[request_id].nonce, verification_url: "#{ISSUER}/verify?request=#{request_id}" }
@@ -72,7 +74,7 @@ module Kiosk
72
74
  # An attestation the provider signed for this principal, as an assistant
73
75
  # submits it to `POST <endpoint>/agents/kyc`.
74
76
  def kyc_attestation(principal, **attributes)
75
- @kyc_provider.attest(principal.user_id, Kiosk.configuration.kyc_audience, attributes)
77
+ @kyc_provider.attest(principal.user_id, @kyc_provider.audience, attributes)
76
78
  end
77
79
 
78
80
  private
@@ -83,15 +85,13 @@ module Kiosk
83
85
 
84
86
  def stand_in_for_kyc_provider
85
87
  config = Kiosk.configuration
86
- @replaced_kyc_settings = SETTINGS.to_h { [it, config.public_send(it)] }
87
- @kyc_provider = Provider.new
88
- config.kyc_provider = @kyc_provider
89
- config.kyc_issuer = ISSUER
90
- config.kyc_public_key = Provider.key
88
+ @replaced_kyc_provider = config.kyc_provider
89
+ @kyc_provider = Provider.new(audience: @replaced_kyc_provider&.audience)
90
+ config.kyc_provider = @kyc_provider
91
91
  end
92
92
 
93
93
  def restore_kyc_provider
94
- @replaced_kyc_settings&.each { |setting, value| Kiosk.configuration.public_send(:"#{setting}=", value) }
94
+ Kiosk.configuration.kyc_provider = @replaced_kyc_provider if @kyc_provider
95
95
  end
96
96
  end
97
97
  end
@@ -22,19 +22,16 @@ module Kiosk
22
22
  base.teardown { SeededDatabase.empty }
23
23
  end
24
24
 
25
+ # Every table on the search path but Rails' own two.
25
26
  def self.empty
26
27
  connection = ActiveRecord::Base.connection
27
- schemas = ActiveRecord.dump_schemas.split(',').map { connection.quote(it.strip) }.join(', ')
28
- tables = connection.select_values(<<~SQL)
29
- SELECT format('%I.%I', schemaname, tablename) FROM pg_tables
30
- WHERE schemaname IN (#{schemas}) AND tablename NOT IN ('schema_migrations', 'ar_internal_metadata')
31
- SQL
32
- connection.execute("TRUNCATE #{tables.join(', ')} RESTART IDENTITY CASCADE") if tables.any?
28
+ tables = connection.tables - %w[schema_migrations ar_internal_metadata]
29
+ connection.execute("TRUNCATE #{tables.map { connection.quote_table_name(it) }.join(', ')} RESTART IDENTITY CASCADE") if tables.any?
33
30
  end
34
31
  end
35
32
 
36
33
  # Serves the app over HTTP from the test process, on a free port that
37
- # becomes the origin's issuer, so a test drives the wire as an assistant does.
34
+ # becomes the origin's issuer, so a test drives the protocol as an assistant does.
38
35
  module LiveServer
39
36
  def self.included(base)
40
37
  base.include SeededDatabase
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+
5
+ require 'kiosk/test_helpers/skill_pin'
6
+
7
+ # `expect(Kiosk.configuration).to pin_the_served_skill(hashed_version: '0.6.28')`
8
+ # The matcher DSL gives the keywords to the block as one Hash.
9
+ RSpec::Matchers.define :pin_the_served_skill do |options|
10
+ match do |configuration|
11
+ @problem = Kiosk::TestHelpers::SkillPin.problem(hashed_version: options.fetch(:hashed_version), configuration:)
12
+ @problem.nil?
13
+ end
14
+
15
+ failure_message { @problem }
16
+ description { 'pin the SHA-256 of the skill that kiosk-server gives' }
17
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk/server'
4
+
5
+ module Kiosk
6
+ module TestHelpers
7
+ # Checks the skill pin of an origin. kiosk-server gives the skill URL, and
8
+ # each kiosk-server release gives the URL of a new skill version. The
9
+ # operator sets `skill_sha256`, the SHA-256 of that file, and tells this
10
+ # check the skill version that the digest is for. When an update of
11
+ # kiosk-server moves the skill to a different version, the check fails and
12
+ # tells the operator how to make the new digest.
13
+ module SkillPin
14
+ VERSION_IN_URL = /skill-v(\d+\.\d+\.\d+)\.md\z/
15
+ SHA256 = /\A\h{64}\z/
16
+
17
+ module_function
18
+
19
+ # nil when the pin is correct. Otherwise, what is wrong and what to do.
20
+ def problem(hashed_version:, configuration: Kiosk.configuration)
21
+ served = Kiosk::Configuration.new.skill_url
22
+ version = served[VERSION_IN_URL, 1]
23
+ return override(configuration.skill_url, served) unless configuration.skill_url == served
24
+ return if hashed_version == version && configuration.skill_sha256.to_s.match?(SHA256)
25
+
26
+ stale(served, version, hashed_version, configuration.skill_sha256)
27
+ end
28
+
29
+ def override(configured, served)
30
+ "The configuration sets skill_url to #{configured}. kiosk-server gives the skill URL " \
31
+ "(#{served}). Remove the line `c.skill_url = ...` from your configuration."
32
+ end
33
+
34
+ def stale(url, version, hashed_version, sha256)
35
+ <<~MESSAGE
36
+ #{cause(version, hashed_version, sha256)}
37
+ Do these steps:
38
+ 1. Download the skill #{version} from kiosk.tech and calculate its SHA-256:
39
+ curl -fsS #{url} | shasum -a 256
40
+ 2. Set `c.skill_sha256` in config/initializers/kiosk.rb to that digest.
41
+ 3. Set `hashed_version: '#{version}'` in this test.
42
+ MESSAGE
43
+ end
44
+
45
+ def cause(version, hashed_version, sha256)
46
+ if hashed_version != version
47
+ "kiosk-server gives the skill #{version}, but skill_sha256 is the digest of the skill #{hashed_version}."
48
+ elsif sha256.nil?
49
+ "kiosk-server gives the skill #{version}, and skill_sha256 is not set."
50
+ else
51
+ "skill_sha256 is not a SHA-256 digest (64 hexadecimal characters): #{sha256.inspect}."
52
+ end
53
+ end
54
+
55
+ # The Minitest assertion:
56
+ # include Kiosk::TestHelpers::SkillPin::Assertions
57
+ # assert_kiosk_skill_pin hashed_version: '0.6.28'
58
+ module Assertions
59
+ def assert_kiosk_skill_pin(hashed_version:)
60
+ problem = SkillPin.problem(hashed_version:)
61
+ assert(problem.nil?, problem)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kiosk
4
4
  module TestHelpers
5
- VERSION = '0.6.1'
5
+ VERSION = '0.6.28'
6
6
  end
7
7
  end
@@ -20,7 +20,7 @@ module Kiosk
20
20
 
21
21
  def [](header) = headers[header.to_s.downcase]
22
22
 
23
- # Whether the client paid a proof-of-work toll and sent the request again.
23
+ # Whether the client solved a proof-of-work toll and sent the request again.
24
24
  def pow_retried? = proofs.positive?
25
25
  end
26
26
 
@@ -43,7 +43,7 @@ module Kiosk
43
43
  http
44
44
  end
45
45
 
46
- # An access token and the key it is bound to. As a header value, the wire
46
+ # An access token and the key it is bound to. As a header value, the protocol
47
47
  # sends it as `Authorization: DPoP <token>` with a proof for that request;
48
48
  # with no token, as the proof alone (a token request).
49
49
  DpopCredential = Data.define(:token, :key)
@@ -95,7 +95,7 @@ module Kiosk
95
95
  uri
96
96
  end
97
97
 
98
- # The proof-of-work challenges of a toll this client pays, or nil.
98
+ # The proof-of-work challenges of a toll this client solves, or nil.
99
99
  def toll_challenges(answer)
100
100
  return unless @pay_tolls && answer.status == 402 && answer.body.is_a?(Hash)
101
101
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kiosk-test-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Pirozhkov
@@ -122,11 +122,11 @@ description: |
122
122
  `run_query`, `run_action`, `pay_action`, `kiosk_seed`), the pluggable
123
123
  executor contract, a NullExecutor for self-tests, and the structured
124
124
  error classes (`RLSDenied`, `QuotaExceeded`, `ExecutorNotConfigured`).
125
- It is wired into RSpec by `kiosk-rls-rspec` and into Minitest by
125
+ It is connected to RSpec by `kiosk-rls-rspec` and into Minitest by
126
126
  `kiosk-rls-minitest`.
127
127
 
128
128
  The ASSISTANT drives an origin over HTTP as an assistant does: it
129
- registers, pays proof-of-work tolls, queries, runs, pays with signed
129
+ registers, solves proof-of-work tolls, queries, runs, pays with signed
130
130
  mandates and listens on the event stream. `Kyc` stands in for the
131
131
  operator's KYC provider, and `StripeMock` fronts a local stripe-mock.
132
132
 
@@ -176,6 +176,8 @@ files:
176
176
  - lib/kiosk/test_helpers/null_executor.rb
177
177
  - lib/kiosk/test_helpers/person.rb
178
178
  - lib/kiosk/test_helpers/pow_solver.rb
179
+ - lib/kiosk/test_helpers/skill_pin.rb
180
+ - lib/kiosk/test_helpers/skill_pin/rspec.rb
179
181
  - lib/kiosk/test_helpers/small_equihash.rb
180
182
  - lib/kiosk/test_helpers/story.rb
181
183
  - lib/kiosk/test_helpers/stripe_mock.rb