kiosk-test-support 0.5.20

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +149 -0
  3. data/LICENSE.txt +202 -0
  4. data/README.md +146 -0
  5. data/lib/kiosk/story_spec.rb +6 -0
  6. data/lib/kiosk/story_test.rb +16 -0
  7. data/lib/kiosk/test_helpers/assistant/events.rb +135 -0
  8. data/lib/kiosk/test_helpers/assistant.rb +151 -0
  9. data/lib/kiosk/test_helpers/conformance/checks/declared_shape.rb +91 -0
  10. data/lib/kiosk/test_helpers/conformance/checks/executes.rb +57 -0
  11. data/lib/kiosk/test_helpers/conformance/checks/helpers.rb +112 -0
  12. data/lib/kiosk/test_helpers/conformance/checks/principal_scope.rb +104 -0
  13. data/lib/kiosk/test_helpers/conformance/checks/routes.rb +94 -0
  14. data/lib/kiosk/test_helpers/conformance/checks.rb +45 -0
  15. data/lib/kiosk/test_helpers/conformance/in_process_origin.rb +121 -0
  16. data/lib/kiosk/test_helpers/conformance/minitest.rb +57 -0
  17. data/lib/kiosk/test_helpers/conformance/null_origin.rb +55 -0
  18. data/lib/kiosk/test_helpers/conformance/outcome.rb +25 -0
  19. data/lib/kiosk/test_helpers/conformance/rspec.rb +100 -0
  20. data/lib/kiosk/test_helpers/conformance/verb.rb +36 -0
  21. data/lib/kiosk/test_helpers/conformance.rb +46 -0
  22. data/lib/kiosk/test_helpers/customer.rb +154 -0
  23. data/lib/kiosk/test_helpers/descriptor_examples.rb +46 -0
  24. data/lib/kiosk/test_helpers/errors.rb +55 -0
  25. data/lib/kiosk/test_helpers/in_process_executor.rb +198 -0
  26. data/lib/kiosk/test_helpers/in_process_wire.rb +90 -0
  27. data/lib/kiosk/test_helpers/journey.rb +98 -0
  28. data/lib/kiosk/test_helpers/kyc.rb +98 -0
  29. data/lib/kiosk/test_helpers/live_server.rb +56 -0
  30. data/lib/kiosk/test_helpers/null_executor.rb +95 -0
  31. data/lib/kiosk/test_helpers/person.rb +52 -0
  32. data/lib/kiosk/test_helpers/small_equihash.rb +18 -0
  33. data/lib/kiosk/test_helpers/story.rb +36 -0
  34. data/lib/kiosk/test_helpers/stripe_mock.rb +146 -0
  35. data/lib/kiosk/test_helpers/version.rb +7 -0
  36. data/lib/kiosk/test_helpers/wire.rb +124 -0
  37. data/lib/kiosk/test_helpers.rb +28 -0
  38. metadata +205 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk/user_identity_providers/devise_session'
4
+ require 'kiosk/test_helpers/customer'
5
+
6
+ module Kiosk
7
+ module TestHelpers
8
+ # Someone signed in on the operator's own site: links assistants to their
9
+ # account, approves the code an assistant shows them, and unlinks one.
10
+ #
11
+ # alice = a_person(email: "alice@example.com", password: "…")
12
+ # diner = alice.links(a_newcomer(as: Diner))
13
+ class Person
14
+ # The signed-in browser session, for the operator's own pages.
15
+ attr_reader :site
16
+
17
+ def initialize(origin, email:, password:)
18
+ @origin = origin
19
+ @site = UserIdentityProviders::DeviseSession.new(origin).sign_in!(email:, password:)
20
+ end
21
+
22
+ # A one-time code for an assistant to redeem.
23
+ def link_code
24
+ status, link = site.post_json('/kiosk/auth/link', {}, { session: true })
25
+ raise "the site minted no link code: #{status} #{link}" unless status == 201
26
+
27
+ link.fetch('link_code')
28
+ end
29
+
30
+ # `customer` redeems a fresh link code, and comes back acting for this person.
31
+ def links(customer) = customer.redeems(link_code)
32
+
33
+ # A newly keyed assistant, linked to this person's account.
34
+ def links_a_newcomer = links(Customer.newcomer(Assistant.new(base_url: @origin)))
35
+
36
+ # Approves the code an assistant showed, on the site's verification page.
37
+ def approves(user_code)
38
+ page = site.get_html("/kiosk/oauth/device/verify?user_code=#{user_code}")
39
+ approved = site.post_form('/kiosk/oauth/device/verify', 'user_code' => user_code, 'decision' => 'approve',
40
+ 'authenticity_token' => site.csrf_token(page.body))
41
+ raise "the site did not approve #{user_code}: #{page.code}, #{approved.code}" unless [page.code, approved.code] == %w[200 200]
42
+ end
43
+
44
+ def unlinks(customer)
45
+ status, = site.post_json('/kiosk/auth/unlink', { agent_id: customer.principal.agent_id }, { session: true })
46
+ raise "the site did not unlink #{customer.principal.agent_id}: #{status}" unless status == 204
47
+ end
48
+
49
+ def visits(path) = site.get_html(path)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk/pow/equihash'
4
+
5
+ module Kiosk
6
+ module TestHelpers
7
+ # Require this from test setup only. It makes the Equihash default params
8
+ # n=96 k=5, so a registration in a test solves in about 0.2 s, not 10 s.
9
+ # Params passed explicitly are unchanged.
10
+ module SmallEquihash
11
+ PARAMS = { n: 96, k: 5 }.freeze
12
+
13
+ def params(n: PARAMS[:n], k: PARAMS[:k]) = super # rubocop:disable Naming/MethodParameterName -- the Equihash parameter names.
14
+ end
15
+ end
16
+ end
17
+
18
+ Kiosk::Pow::Equihash.singleton_class.prepend(Kiosk::TestHelpers::SmallEquihash)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk/test_helpers/customer'
4
+ require 'kiosk/test_helpers/live_server'
5
+
6
+ module Kiosk
7
+ module TestHelpers
8
+ autoload :Person, 'kiosk/test_helpers/person'
9
+
10
+ # A test that tells a business story: who comes to the origin, what they
11
+ # ask for, what the origin answers and what they learn afterwards.
12
+ module Story
13
+ def self.included(base)
14
+ base.include LiveServer
15
+ base.public_send(base.respond_to?(:teardown) ? :teardown : :after) { @assistant&.disconnect }
16
+ end
17
+
18
+ # A newly registered assistant, as `as` (a demo's own Customer subclass).
19
+ def a_customer(as: Customer) = as.new(assistant, register)
20
+
21
+ # An assistant holding a key and no account here yet.
22
+ def a_newcomer(as: Customer) = as.newcomer(assistant)
23
+
24
+ # Someone with an account on the operator's site, signed in there.
25
+ def a_person(email:, password:) = Person.new(live_url, email:, password:)
26
+
27
+ # What the origin publishes at `path` to anyone, with no credential.
28
+ def published(path)
29
+ status, body = assistant.wire.get_json(path)
30
+ raise "GET #{path} with no credential answered #{status}" unless status == 200
31
+
32
+ body
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'socket'
6
+ require 'uri'
7
+
8
+ module Kiosk
9
+ module TestHelpers
10
+ # A local stripe-mock for demo suites. A front on {url} relays to it and
11
+ # answers two calls as Stripe does where stripe-mock does not: a confirmed
12
+ # PaymentIntent create with a test card is `succeeded`, fully received, and
13
+ # a refund that names no charge is a 400.
14
+ module StripeMock
15
+ LOG = '/tmp/stripe-mock.log'
16
+ DROPPED = %w[accept-encoding connection content-length host transfer-encoding].freeze
17
+ NO_CHARGE_BODY = JSON.generate(error: {
18
+ type: 'invalid_request_error',
19
+ message: 'One of the following params should be provided for this request: ' \
20
+ 'payment_intent or charge.'
21
+ })
22
+ NO_CHARGE = "HTTP/1.1 400 Bad Request\r\ncontent-type: application/json\r\n" \
23
+ "content-length: #{NO_CHARGE_BODY.bytesize}\r\nconnection: close\r\n\r\n#{NO_CHARGE_BODY}".freeze
24
+
25
+ module_function
26
+
27
+ # The front's base URL: STRIPE_MOCK_URL when set, else a free port held
28
+ # for the life of this process. A suite that spawns its front stops it at
29
+ # exit, so each concurrent suite runs its own.
30
+ def url = @url ||= ENV.fetch('STRIPE_MOCK_URL') { "http://127.0.0.1:#{free_port}" }
31
+
32
+ # Reuses a front already listening on {url}; otherwise spawns stripe-mock
33
+ # and the front, stopped at exit.
34
+ #
35
+ # @return [String] the front's base URL
36
+ def start
37
+ return url if listening?(port)
38
+
39
+ abort 'stripe-mock not found. Install it: brew install stripe-mock' unless system('command -v stripe-mock >/dev/null 2>&1')
40
+
41
+ upstream = free_port
42
+ pids = spawn_processes(upstream)
43
+ at_exit { pids.each { stop(it) } }
44
+ await_ready(upstream)
45
+ end
46
+
47
+ def port = URI(url).port
48
+
49
+ def free_port = TCPServer.open('127.0.0.1', 0) { it.addr[1] }
50
+
51
+ def spawn_processes(upstream)
52
+ [spawn('stripe-mock', '-http-port', upstream.to_s, out: LOG, err: LOG),
53
+ spawn(RbConfig.ruby, '-I', File.expand_path('../..', __dir__), '-rkiosk/test_helpers/stripe_mock',
54
+ '-e', "Kiosk::TestHelpers::StripeMock.serve(#{port}, #{upstream})", out: LOG, err: LOG)]
55
+ end
56
+
57
+ def stop(pid)
58
+ Process.kill('TERM', pid)
59
+ Process.wait(pid)
60
+ rescue Errno::ESRCH, Errno::ECHILD
61
+ nil
62
+ end
63
+
64
+ def await_ready(upstream)
65
+ 30.times do
66
+ return url if listening?(port) && listening?(upstream)
67
+
68
+ sleep 0.3
69
+ end
70
+ abort "stripe-mock did not become ready on #{url} — see #{LOG}"
71
+ end
72
+
73
+ def listening?(port)
74
+ TCPSocket.new('127.0.0.1', port).close
75
+ true
76
+ rescue StandardError
77
+ false
78
+ end
79
+
80
+ # Runs the front until killed.
81
+ def serve(port, upstream)
82
+ @upstream = upstream
83
+ server = TCPServer.new('127.0.0.1', port)
84
+ loop { Thread.new(server.accept) { |client| relay(client) } }
85
+ end
86
+
87
+ def relay(client)
88
+ method, path = client.gets.to_s.split
89
+ return unless path # a readiness check connects and hangs up
90
+
91
+ headers = read_headers(client)
92
+ body = client.read(headers['content-length'].to_i)
93
+ return client.write(NO_CHARGE) if chargeless_refund?(method, path, body)
94
+
95
+ relay_upstream(client, method, path, body, headers)
96
+ ensure
97
+ client.close
98
+ end
99
+
100
+ def relay_upstream(client, method, path, body, headers)
101
+ upstream = forward(method, path, body, headers)
102
+ answer = upstream.body.to_s
103
+ answer = settled(answer) if confirmed_create?(method, path, body)
104
+ respond(client, upstream, answer)
105
+ end
106
+
107
+ def chargeless_refund?(method, path, body)
108
+ params = URI.decode_www_form(body.to_s).to_h
109
+ method == 'POST' && path == '/v1/refunds' && params['payment_intent'].to_s.empty? && params['charge'].to_s.empty?
110
+ end
111
+
112
+ def forward(method, path, body, headers)
113
+ Net::HTTP.start('127.0.0.1', @upstream) do |http|
114
+ http.send_request(method, path, body, headers.except(*DROPPED))
115
+ end
116
+ end
117
+
118
+ def read_headers(client)
119
+ headers = {}
120
+ while (line = client.gets) && line != "\r\n"
121
+ name, value = line.split(':', 2)
122
+ headers[name.downcase] = value.strip
123
+ end
124
+ headers
125
+ end
126
+
127
+ def confirmed_create?(method, path, body)
128
+ method == 'POST' && path == '/v1/payment_intents' &&
129
+ URI.decode_www_form(body.to_s).to_h['confirm'] == 'true'
130
+ end
131
+
132
+ def respond(client, upstream, answer)
133
+ client.write("HTTP/1.1 #{upstream.code} #{upstream.message}\r\n")
134
+ upstream.each_header { |k, v| client.write("#{k}: #{v}\r\n") unless DROPPED.include?(k) || k == 'content-encoding' }
135
+ client.write("content-length: #{answer.bytesize}\r\nconnection: close\r\n\r\n#{answer}")
136
+ end
137
+
138
+ def settled(answer)
139
+ intent = JSON.parse(answer)
140
+ return answer unless intent['object'] == 'payment_intent'
141
+
142
+ JSON.generate(intent.merge('status' => 'succeeded', 'amount_received' => intent['amount']))
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiosk
4
+ module TestHelpers
5
+ VERSION = '0.5.20'
6
+ end
7
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'kiosk/pow/equihash/solver'
7
+
8
+ module Kiosk
9
+ module TestHelpers
10
+ # Any method at any path of an origin, with the whole answer returned. With
11
+ # `pay_tolls: true` it solves a proof-of-work toll and sends the request once more.
12
+ #
13
+ # wire = Kiosk::TestHelpers::Wire.new(base_url: "http://127.0.0.1:3006")
14
+ # status, problem = wire.post_json("/kiosk/edit_listing", { listing_id: "junk" }, Wire.bearer(token))
15
+ class Wire
16
+ # One answer: a body that is not JSON reads as `{}`, an unreachable origin as status 0.
17
+ Response = Data.define(:status, :headers, :body, :raw_body, :proofs) do
18
+ def initialize(status:, body:, headers: {}, raw_body: '', proofs: 0) = super
19
+
20
+ def [](header) = headers[header.to_s.downcase]
21
+
22
+ # Whether the client paid a proof-of-work toll and sent the request again.
23
+ def pow_retried? = proofs.positive?
24
+ end
25
+
26
+ METHODS = {
27
+ get: Net::HTTP::Get,
28
+ post: Net::HTTP::Post,
29
+ put: Net::HTTP::Put,
30
+ patch: Net::HTTP::Patch,
31
+ delete: Net::HTTP::Delete,
32
+ head: Net::HTTP::Head,
33
+ options: Net::HTTP::Options
34
+ }.freeze
35
+
36
+ # The scheme decides TLS.
37
+ def self.http_for(uri, open_timeout: nil, read_timeout: nil)
38
+ http = Net::HTTP.new(uri.hostname, uri.port)
39
+ http.use_ssl = uri.scheme == 'https'
40
+ http.open_timeout = open_timeout if open_timeout
41
+ http.read_timeout = read_timeout if read_timeout
42
+ http
43
+ end
44
+
45
+ def self.bearer(token) = { 'Authorization' => "Bearer #{token}" }
46
+
47
+ # A solved proof of work for each challenge, as the `Kiosk-PoW` header carries them.
48
+ def self.solve(challenges) = challenges.map { { challenge: it, nonce: Kiosk::Pow::Equihash.solve(it) } }
49
+
50
+ attr_reader :base_url
51
+
52
+ def initialize(base_url:, pay_tolls: false, open_timeout: 10, read_timeout: 30)
53
+ @base_url = base_url.to_s.chomp('/')
54
+ @pay_tolls = pay_tolls
55
+ @open_timeout = open_timeout
56
+ @read_timeout = read_timeout
57
+ end
58
+
59
+ def bearer(token) = self.class.bearer(token)
60
+
61
+ def post_json(path, body = {}, headers = {}) = post(path, body, headers).then { [it.status, it.body] }
62
+
63
+ def get_json(path, params = {}, headers = {}) = get(path, params, headers).then { [it.status, it.body] }
64
+
65
+ def post(path, body = {}, headers = {}) = request(:post, path, body:, headers:)
66
+
67
+ def get(path, params = {}, headers = {}) = request(:get, path, params:, headers:)
68
+
69
+ def post_form(path, form, headers = {})
70
+ request(:post, path, headers:) { it.set_form_data(form) }
71
+ end
72
+
73
+ def request(method, path, body: nil, params: nil, headers: {}, &form)
74
+ klass = METHODS.fetch(method.to_s.downcase.to_sym) { raise ArgumentError, "unsupported HTTP method #{method.inspect}" }
75
+ uri = request_uri(path, params)
76
+ answer = deliver(klass, uri, body, headers, &form)
77
+ challenges = toll_challenges(answer)
78
+ return answer unless challenges
79
+
80
+ proofs = self.class.solve(challenges)
81
+ deliver(klass, uri, body, headers.merge('Kiosk-PoW' => JSON.generate(proofs)), &form).with(proofs: proofs.size)
82
+ end
83
+
84
+ private
85
+
86
+ def request_uri(path, params)
87
+ uri = URI("#{@base_url}#{path}")
88
+ uri.query = URI.encode_www_form(params) if params&.any?
89
+ uri
90
+ end
91
+
92
+ # The proof-of-work challenges of a toll this client pays, or nil.
93
+ def toll_challenges(answer)
94
+ return unless @pay_tolls && answer.status == 402 && answer.body.is_a?(Hash)
95
+
96
+ challenges = answer.body['challenges']
97
+ challenges if challenges.is_a?(Array) && challenges.any?
98
+ end
99
+
100
+ def deliver(klass, uri, body, headers)
101
+ request = klass.new(uri, body.nil? ? headers : { 'Content-Type' => 'application/json' }.merge(headers))
102
+ request.body = JSON.generate(body) unless body.nil?
103
+ yield request if block_given?
104
+ begin
105
+ answer = self.class.http_for(uri, open_timeout: @open_timeout, read_timeout: @read_timeout).request(request)
106
+ rescue StandardError => e
107
+ return Response.new(status: 0, body: { 'error' => e.class.name, 'detail' => e.message })
108
+ end
109
+ response(answer)
110
+ end
111
+
112
+ def response(answer)
113
+ raw_body = answer.body.to_s
114
+ body = begin
115
+ JSON.parse(raw_body)
116
+ rescue JSON::ParserError
117
+ {}
118
+ end
119
+ headers = answer.each_header.to_h { |name, value| [name.downcase, value] }
120
+ Response.new(status: answer.code.to_i, headers:, body:, raw_body:)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk'
4
+
5
+ require 'kiosk/test_helpers/version'
6
+ require 'kiosk/test_helpers/errors'
7
+ require 'kiosk/test_helpers/null_executor'
8
+ require 'kiosk/test_helpers/journey'
9
+ require 'kiosk/test_helpers/conformance'
10
+
11
+ module Kiosk
12
+ # Test support for Kiosk operators: the journey DSL over a pluggable executor,
13
+ # and the conformance checks an origin runs against itself.
14
+ module TestHelpers
15
+ class << self
16
+ # Anything answering the {NullExecutor} contract.
17
+ attr_accessor :executor
18
+
19
+ def require_executor!
20
+ executor || raise(Errors::ExecutorNotConfigured)
21
+ end
22
+
23
+ def reset!
24
+ @executor = nil
25
+ end
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kiosk-test-support
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.20
5
+ platform: ruby
6
+ authors:
7
+ - Phil Pirozhkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json_schemer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.3'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.3'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: jwt
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '4.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '4.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: kiosk-core
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: 0.5.0
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: 0.5.0
66
+ - !ruby/object:Gem::Dependency
67
+ name: kiosk-pow-equihash
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.5.0
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: 0.5.0
80
+ - !ruby/object:Gem::Dependency
81
+ name: puma
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '5.0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '5.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: websocket-driver
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '0.7'
101
+ type: :runtime
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '0.7'
108
+ description: |
109
+ kiosk-test-support is the framework-agnostic test surface a Kiosk
110
+ provider uses on their OWN application.
111
+
112
+ It carries two things. The CONFORMANCE CHECKS assert the four
113
+ properties the protocol makes normative of an origin: that its routes
114
+ resolve, that a verb executes, that a query answers the shape it
115
+ declared, and that data access is scoped to the authenticated
116
+ principal. They ship with a Minitest adapter and an RSpec adapter, both
117
+ loaded by explicit require, so the same fault reads identically in
118
+ either framework.
119
+
120
+ The JOURNEY DSL carries the identity-scoped call helpers
121
+ (`as_agent_of`, `as_user`, `as_agent`, `as_anonymous`, `query`,
122
+ `run_query`, `run_action`, `pay_action`, `kiosk_seed`), the pluggable
123
+ executor contract, a NullExecutor for self-tests, and the structured
124
+ error classes (`RLSDenied`, `QuotaExceeded`, `ExecutorNotConfigured`).
125
+ It is wired into RSpec by `kiosk-rls-rspec` and into Minitest by
126
+ `kiosk-rls-minitest`.
127
+
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
130
+ mandates and listens on the event stream. `Kyc` stands in for the
131
+ operator's KYC provider, and `StripeMock` fronts a local stripe-mock.
132
+
133
+ No Postgres, no Rails, no test-framework dependency.
134
+ `InProcessExecutor` and `Conformance::InProcessOrigin` run against
135
+ the host's kiosk-server and are required explicitly; this gem does
136
+ not depend on kiosk-server.
137
+ email:
138
+ - hello@fili.pp.ru
139
+ executables: []
140
+ extensions: []
141
+ extra_rdoc_files: []
142
+ files:
143
+ - CHANGELOG.md
144
+ - LICENSE.txt
145
+ - README.md
146
+ - lib/kiosk/story_spec.rb
147
+ - lib/kiosk/story_test.rb
148
+ - lib/kiosk/test_helpers.rb
149
+ - lib/kiosk/test_helpers/assistant.rb
150
+ - lib/kiosk/test_helpers/assistant/events.rb
151
+ - lib/kiosk/test_helpers/conformance.rb
152
+ - lib/kiosk/test_helpers/conformance/checks.rb
153
+ - lib/kiosk/test_helpers/conformance/checks/declared_shape.rb
154
+ - lib/kiosk/test_helpers/conformance/checks/executes.rb
155
+ - lib/kiosk/test_helpers/conformance/checks/helpers.rb
156
+ - lib/kiosk/test_helpers/conformance/checks/principal_scope.rb
157
+ - lib/kiosk/test_helpers/conformance/checks/routes.rb
158
+ - lib/kiosk/test_helpers/conformance/in_process_origin.rb
159
+ - lib/kiosk/test_helpers/conformance/minitest.rb
160
+ - lib/kiosk/test_helpers/conformance/null_origin.rb
161
+ - lib/kiosk/test_helpers/conformance/outcome.rb
162
+ - lib/kiosk/test_helpers/conformance/rspec.rb
163
+ - lib/kiosk/test_helpers/conformance/verb.rb
164
+ - lib/kiosk/test_helpers/customer.rb
165
+ - lib/kiosk/test_helpers/descriptor_examples.rb
166
+ - lib/kiosk/test_helpers/errors.rb
167
+ - lib/kiosk/test_helpers/in_process_executor.rb
168
+ - lib/kiosk/test_helpers/in_process_wire.rb
169
+ - lib/kiosk/test_helpers/journey.rb
170
+ - lib/kiosk/test_helpers/kyc.rb
171
+ - lib/kiosk/test_helpers/live_server.rb
172
+ - lib/kiosk/test_helpers/null_executor.rb
173
+ - lib/kiosk/test_helpers/person.rb
174
+ - lib/kiosk/test_helpers/small_equihash.rb
175
+ - lib/kiosk/test_helpers/story.rb
176
+ - lib/kiosk/test_helpers/stripe_mock.rb
177
+ - lib/kiosk/test_helpers/version.rb
178
+ - lib/kiosk/test_helpers/wire.rb
179
+ homepage: https://kiosk.tech
180
+ licenses:
181
+ - Apache-2.0
182
+ metadata:
183
+ homepage_uri: https://kiosk.tech
184
+ source_code_uri: https://github.com/kiosk-hq/kiosk
185
+ changelog_uri: https://github.com/kiosk-hq/kiosk/blob/main/kiosk-test-support/CHANGELOG.md
186
+ bug_tracker_uri: https://github.com/kiosk-hq/kiosk/issues
187
+ rubygems_mfa_required: 'true'
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '4.0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubygems_version: 4.0.3
203
+ specification_version: 4
204
+ summary: Kiosk conformance checks and journey-test DSL for RSpec and Minitest
205
+ test_files: []