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 +4 -4
- data/CHANGELOG.md +113 -0
- data/README.md +66 -23
- data/lib/kiosk/story_test.rb +1 -1
- data/lib/kiosk/test_helpers/assistant.rb +3 -3
- data/lib/kiosk/test_helpers/conformance/checks/routes.rb +1 -1
- data/lib/kiosk/test_helpers/conformance/in_process_origin.rb +1 -1
- data/lib/kiosk/test_helpers/customer.rb +2 -2
- data/lib/kiosk/test_helpers/errors.rb +2 -2
- data/lib/kiosk/test_helpers/in_process_wire.rb +3 -3
- data/lib/kiosk/test_helpers/kyc.rb +13 -13
- data/lib/kiosk/test_helpers/live_server.rb +4 -7
- data/lib/kiosk/test_helpers/skill_pin/rspec.rb +17 -0
- data/lib/kiosk/test_helpers/skill_pin.rb +66 -0
- data/lib/kiosk/test_helpers/version.rb +1 -1
- data/lib/kiosk/test_helpers/wire.rb +3 -3
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1a0b3e7ba79f9c1cf3f0e0ed29b2d9c7ed3deb87eed7deae9e030637d177903
|
|
4
|
+
data.tar.gz: 9df61c38d3daefdc760b862cdc84e071e049ee0753172dde44877585e9949161
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
8
|
-
- `Kiosk::TestHelpers::Wire` — the same transport without
|
|
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"
|
|
23
|
+
gem "kiosk-rls-rspec" # RSpec
|
|
25
24
|
# or
|
|
26
|
-
gem "kiosk-rls-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
|
|
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
|
-
|
|
38
|
-
run_action :create_order, items: ["bread"]
|
|
39
|
-
end
|
|
41
|
+
kiosk_seed :orders, owner: alice, item: "bread"
|
|
40
42
|
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
expect
|
|
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
|
-
##
|
|
55
|
+
## Configuring an executor
|
|
53
56
|
|
|
54
|
-
The DSL is inert until you
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
data/lib/kiosk/story_test.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Kiosk
|
|
|
8
8
|
class StoryTest < ActiveSupport::TestCase
|
|
9
9
|
include TestHelpers::Story
|
|
10
10
|
|
|
11
|
-
# Asserts the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
@
|
|
87
|
-
@kyc_provider = Provider.new
|
|
88
|
-
config.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
|
-
|
|
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
|
-
|
|
28
|
-
tables
|
|
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
|
|
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
|
|
@@ -20,7 +20,7 @@ module Kiosk
|
|
|
20
20
|
|
|
21
21
|
def [](header) = headers[header.to_s.downcase]
|
|
22
22
|
|
|
23
|
-
# Whether the client
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|