haveapi 0.31.1 → 0.32.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/benchmarks/documentation.rb +29 -6
- data/benchmarks/endpoints.rb +153 -24
- data/benchmarks/spec/client_examples_spec.rb +109 -37
- data/doc/client-examples.md +218 -45
- data/doc/documentation-oauth.md +157 -0
- data/doc/documentation-ui.md +192 -0
- data/doc/index.md +3 -6
- data/doc/performance.md +39 -12
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/authentication/chain.rb +19 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
- data/lib/haveapi/client_example.rb +143 -15
- data/lib/haveapi/client_examples/curl.rb +30 -20
- data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
- data/lib/haveapi/client_examples/go_client.rb +9 -2
- data/lib/haveapi/client_examples/http.rb +9 -4
- data/lib/haveapi/client_examples/js_async_client.rb +7 -0
- data/lib/haveapi/client_examples/js_client.rb +47 -36
- data/lib/haveapi/client_examples/php_client.rb +161 -75
- data/lib/haveapi/client_examples/python_async_client.rb +3 -0
- data/lib/haveapi/client_examples/python_client.rb +61 -47
- data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
- data/lib/haveapi/client_examples/ruby_client.rb +80 -31
- data/lib/haveapi/client_examples/rust_client.rb +20 -9
- data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
- data/lib/haveapi/client_examples/ts_client.rb +3 -0
- data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
- data/lib/haveapi/documentation_context.rb +7 -2
- data/lib/haveapi/documentation_helpers.rb +223 -0
- data/lib/haveapi/documentation_oauth.rb +399 -0
- data/lib/haveapi/documentation_search.rb +225 -0
- data/lib/haveapi/documentation_search_index.rb +79 -0
- data/lib/haveapi/documentation_validators.rb +179 -0
- data/lib/haveapi/public/css/documentation.css +343 -0
- data/lib/haveapi/public/js/documentation.js +992 -0
- data/lib/haveapi/server.rb +144 -86
- data/lib/haveapi/shared_documentation_helpers.rb +54 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +16 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
- data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
- data/lib/haveapi/views/documentation/search_form.erb +10 -0
- data/lib/haveapi/views/documentation/search_index.erb +1 -0
- data/lib/haveapi/views/documentation/search_results.erb +20 -0
- data/lib/haveapi/views/index.erb +2 -4
- data/lib/haveapi/views/main_layout.erb +12 -6
- data/lib/haveapi/views/shared_documentation.erb +12 -0
- data/lib/haveapi/views/shared_sidebar.erb +25 -0
- data/lib/haveapi/views/version_page/auth_body.erb +12 -33
- data/lib/haveapi/views/version_page/client_overview.erb +40 -0
- data/lib/haveapi/views/version_page/example_panels.erb +18 -0
- data/lib/haveapi/views/version_page/resource_body.erb +91 -132
- data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
- data/lib/haveapi/views/version_page/usage.erb +7 -0
- data/lib/haveapi/views/version_page.erb +36 -117
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
- data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
- data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
- data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
- data/lib/haveapi/views/version_sidebar.erb +59 -48
- data/spec/authentication/basic_spec.rb +10 -10
- data/spec/authentication/oauth2_chain_spec.rb +72 -0
- data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
- data/spec/client_examples/assets/documentation_test.cjs +817 -0
- data/spec/client_examples/context_performance_spec.rb +5 -4
- data/spec/client_examples/context_spec.rb +23 -2
- data/spec/client_examples/detail_spec.rb +175 -0
- data/spec/client_examples/dynamic_acceptance.md +59 -26
- data/spec/client_examples/dynamic_acceptance.rb +164 -44
- data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
- data/spec/client_examples/dynamic_fixture.rb +4 -3
- data/spec/client_examples/dynamic_js_spec.rb +20 -11
- data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
- data/spec/client_examples/dynamic_python_spec.rb +62 -11
- data/spec/client_examples/go_spec.rb +14 -1
- data/spec/client_examples/presentation_spec.rb +247 -0
- data/spec/client_examples/profile_mount_spec.rb +40 -13
- data/spec/client_examples/rust_client_spec.rb +35 -1
- data/spec/client_examples/shell_spec.rb +268 -0
- data/spec/client_examples/syntax_spec.rb +192 -0
- data/spec/client_examples/ts_generated_client_spec.rb +15 -2
- data/spec/client_examples/typescript_profiles_spec.rb +1 -1
- data/spec/documentation/auth_filtering_spec.rb +9 -4
- data/spec/documentation/canonical_routes_spec.rb +89 -0
- data/spec/documentation/client_examples_spec.rb +39 -5
- data/spec/documentation/client_overview_spec.rb +145 -0
- data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
- data/spec/documentation/example_variants_spec.rb +93 -0
- data/spec/documentation/fixtures/search_index.json +476 -0
- data/spec/documentation/identity_and_guides_spec.rb +130 -0
- data/spec/documentation/identity_context_spec.rb +180 -0
- data/spec/documentation/navigation_spec.rb +296 -0
- data/spec/documentation/oauth_fixture_spec.rb +39 -0
- data/spec/documentation/oauth_spec.rb +912 -0
- data/spec/documentation/oauth_transport_spec.rb +45 -0
- data/spec/documentation/performance_spec.rb +47 -6
- data/spec/documentation/reference_headings_spec.rb +190 -0
- data/spec/documentation/search_index_routes_spec.rb +202 -0
- data/spec/documentation/search_index_spec.rb +85 -0
- data/spec/documentation/search_routes_spec.rb +199 -0
- data/spec/documentation/search_spec.rb +134 -0
- data/spec/documentation/shared_pages_spec.rb +240 -0
- data/spec/documentation/validators_spec.rb +123 -0
- data/spec/documentation_spec.rb +3 -3
- data/spec/packaging_spec.rb +21 -2
- data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
- data/spec/performance/documentation_shapes_spec.rb +97 -0
- data/spec/performance/json_activation_spec.rb +78 -1
- data/spec/performance/release_lineage_spec.rb +4 -3
- data/spec/performance/runner_spec.rb +1 -0
- data/test_support/all_client_performance_fixture.rb +6 -10
- data/test_support/documentation_oauth_fixture.rb +54 -0
- data/test_support/documentation_performance_fixture.rb +19 -1
- data/test_support/dynamic_documentation_api.rb +35 -10
- data/test_support/dynamic_python_login_spec.rb +3 -11
- metadata +52 -3
|
@@ -30,11 +30,12 @@ describe HaveAPI::ClientExample::Context, '#with' do
|
|
|
30
30
|
it 'shares every unchanged field and owns a different snapshot for a new request' do
|
|
31
31
|
parent = parent_context(8)
|
|
32
32
|
other_request = parent_context(8)
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
changes = [{ profile: }, { sample: }, { detail: :thorough }]
|
|
34
|
+
children = changes.map { |change| parent.with(**change) }
|
|
35
|
+
fields = described_class::FIELDS
|
|
35
36
|
|
|
36
37
|
children.each_with_index do |child, index|
|
|
37
|
-
changed = index
|
|
38
|
+
changed = changes[index].keys.first
|
|
38
39
|
(fields - [changed]).each do |field|
|
|
39
40
|
expect(child.public_send(field)).to equal(parent.public_send(field))
|
|
40
41
|
end
|
|
@@ -51,7 +52,7 @@ describe HaveAPI::ClientExample::Context, '#with' do
|
|
|
51
52
|
it 'keeps allocations independent of unrelated description size for small changes' do
|
|
52
53
|
small = parent_context(8)
|
|
53
54
|
large = parent_context(128)
|
|
54
|
-
changes = [{ profile: }, { sample: }]
|
|
55
|
+
changes = [{ profile: }, { sample: }, { detail: :thorough }]
|
|
55
56
|
[small, large].each do |parent|
|
|
56
57
|
changes.each { |change| 80.times { parent.with(**change) } }
|
|
57
58
|
end
|
|
@@ -25,6 +25,27 @@ describe HaveAPI::ClientExample::Context do
|
|
|
25
25
|
expect(child).to be_frozen
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
it 'defaults to simple and shares owned data when changing the detail' do
|
|
29
|
+
expect(context.detail).to eq(:simple)
|
|
30
|
+
thorough = context.with(detail: :thorough)
|
|
31
|
+
expect(thorough.detail).to eq(:thorough)
|
|
32
|
+
expect(thorough).to be_frozen
|
|
33
|
+
(described_class::FIELDS - [:detail]).each do |field|
|
|
34
|
+
expect(thorough.public_send(field)).to equal(context.public_send(field))
|
|
35
|
+
end
|
|
36
|
+
expect(thorough.with(detail: :simple).detail).to eq(:simple)
|
|
37
|
+
expect(context.detail).to eq(:simple)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'rejects detail values outside the two explicit symbols in construction and derivation' do
|
|
41
|
+
[nil, false, 'simple', 'thorough', :unknown].each do |detail|
|
|
42
|
+
expect { context.with(detail:) }.to raise_error(ArgumentError, 'detail must be :simple or :thorough')
|
|
43
|
+
expect do
|
|
44
|
+
described_class.new(version: '1', host: 'api.test', base_url: '/', version_url: '/v1/', description: {}, detail:)
|
|
45
|
+
end.to raise_error(ArgumentError, 'detail must be :simple or :thorough')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
28
49
|
describe '#with' do
|
|
29
50
|
let(:fields) do
|
|
30
51
|
{
|
|
@@ -33,7 +54,7 @@ describe HaveAPI::ClientExample::Context do
|
|
|
33
54
|
description: { 'resources' => { 'widget' => { 'label' => 'Widget' } } },
|
|
34
55
|
resource_path: [:widget, 2], resource: { 'label' => 'Widget' },
|
|
35
56
|
action_name: :show, action: { 'method' => 'GET' },
|
|
36
|
-
sample: { 'request' => { 'values' => [0, false, '', nil] } }, profile: Object.new
|
|
57
|
+
sample: { 'request' => { 'values' => [0, false, '', nil] } }, profile: Object.new, detail: :simple
|
|
37
58
|
}
|
|
38
59
|
end
|
|
39
60
|
let(:parent) { described_class.new(**fields) }
|
|
@@ -45,7 +66,7 @@ describe HaveAPI::ClientExample::Context do
|
|
|
45
66
|
resource_path: [:other, 3], resource: { 'label' => 'Other' },
|
|
46
67
|
action_name: :update, action: { 'method' => 'POST' },
|
|
47
68
|
sample: { 'request' => { 'values' => [false, 0, nil, ''] } },
|
|
48
|
-
profile: Object.new
|
|
69
|
+
profile: Object.new, detail: :thorough
|
|
49
70
|
}
|
|
50
71
|
end
|
|
51
72
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require_relative 'dynamic_fixture'
|
|
4
|
+
|
|
5
|
+
RSpec.describe HaveAPI::ClientExample, '.render' do
|
|
6
|
+
include DynamicExampleFixture
|
|
7
|
+
|
|
8
|
+
let(:renderers) do
|
|
9
|
+
[HaveAPI::ClientExamples::JsClient, HaveAPI::ClientExamples::JsAsyncClient,
|
|
10
|
+
HaveAPI::ClientExamples::TsClient, HaveAPI::ClientExamples::TsAsyncClient,
|
|
11
|
+
HaveAPI::ClientExamples::PythonClient, HaveAPI::ClientExamples::PythonAsyncClient]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'keeps simple actions focused on the existing api and leaves setup and diagnostics to thorough' do
|
|
15
|
+
renderers.each do |renderer|
|
|
16
|
+
context = dynamic_context
|
|
17
|
+
simple = renderer.render(:example, context:).fetch(:code)
|
|
18
|
+
thorough = renderer.render(:example, context: context.with(detail: :thorough)).fetch(:code)
|
|
19
|
+
expect(simple).to include('api', 'name/with space')
|
|
20
|
+
expect(simple).not_to include('import ', 'credential(', 'os.environ', 'new AsyncClient', 'new CallbackClient',
|
|
21
|
+
'with Client(', 'with AsyncClient(', 'try:', 'try {', 'response.meta', 'reply.meta()')
|
|
22
|
+
expect(thorough).to include('import ', context.base_url)
|
|
23
|
+
expect(thorough).to include(renderer.code == :python ? 'except HaveAPIError' : 'catch (error)')
|
|
24
|
+
expect(context.detail).to eq(:simple)
|
|
25
|
+
expect(context.sample[:request][:enabled]).to be(false)
|
|
26
|
+
expect(context.sample[:request][:count]).to eq(0)
|
|
27
|
+
expect(context.sample[:request][:nullable]).to be_nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns identical limitations at both levels without revealing hidden sample or metadata values' do
|
|
32
|
+
contexts = [
|
|
33
|
+
dynamic_context(sample: dynamic_sample.merge(path_params: [7])),
|
|
34
|
+
dynamic_context(sample: dynamic_sample.merge(request: { hidden_parameter: 'private-sample' })),
|
|
35
|
+
dynamic_context(sample: { request: nil, response: nil }),
|
|
36
|
+
dynamic_context(action: dynamic_action.merge(meta: {
|
|
37
|
+
global: { input: { parameters: { secret: { required: true, default: 'private-default' } } } }
|
|
38
|
+
}))
|
|
39
|
+
]
|
|
40
|
+
renderers.each do |renderer|
|
|
41
|
+
contexts.each do |context|
|
|
42
|
+
simple = renderer.render(:example, context:)
|
|
43
|
+
thorough = renderer.render(:example, context: context.with(detail: :thorough))
|
|
44
|
+
expect(simple).to eq(thorough)
|
|
45
|
+
expect(simple[:status]).to eq(:limitation)
|
|
46
|
+
expect(simple[:reason]).not_to include('private-sample', 'private-default', 'hidden_parameter')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'validates unsupported required authentication before returning either action fragment' do
|
|
52
|
+
context = dynamic_context(action: dynamic_action.merge(auth: true), description: { authentication: {} })
|
|
53
|
+
renderers.each do |renderer|
|
|
54
|
+
results = %i[simple thorough].map { |detail| renderer.render(:example, context: context.with(detail:)) }
|
|
55
|
+
expect(results[0]).to eq(results[1])
|
|
56
|
+
expect(results[0][:status]).to eq(:limitation)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'keeps Python async fragments in an explicit async context without adding executable wrappers' do
|
|
61
|
+
source = HaveAPI::ClientExamples::PythonAsyncClient.render(:example, context: dynamic_context).fetch(:code)
|
|
62
|
+
script = 'import ast, sys; ast.parse("async def example():\n" + "".join(" " + line for line in sys.stdin))'
|
|
63
|
+
output, status = Open3.capture2e('python', '-c', script, stdin_data: source)
|
|
64
|
+
expect(status).to be_success, output
|
|
65
|
+
expect(source).to include('# Run inside an async function after initializing api.', 'response = await api.resources')
|
|
66
|
+
expect(source).not_to include('__main__', 'async def ', 'asyncio.run')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'preserves operation waiting and its timeout meaning at both detail levels' do
|
|
70
|
+
context = dynamic_context(action: dynamic_action.merge(blocking: true))
|
|
71
|
+
[HaveAPI::ClientExamples::PythonClient, HaveAPI::ClientExamples::PythonAsyncClient].each do |renderer|
|
|
72
|
+
%i[simple thorough].each do |detail|
|
|
73
|
+
source = renderer.render(:example, context: context.with(detail:)).fetch(:code)
|
|
74
|
+
expect(source).to include('response.operation.wait(timeout=300, interval=1)', 'does not cancel a server operation')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'uses simple fragments for positional dynamic, Ruby and PHP renderers without changing their calls' do
|
|
80
|
+
context = dynamic_context
|
|
81
|
+
args = [context.host, context.base_url, context.version, context.resource_path, context.resource,
|
|
82
|
+
context.action_name, context.action]
|
|
83
|
+
renderers.each do |renderer|
|
|
84
|
+
expect(renderer.new(*args).example(context.sample)).to eq(renderer.render(:example, context:))
|
|
85
|
+
end
|
|
86
|
+
sample = { path_params: [7], request: { name: '' }, status: false }
|
|
87
|
+
action = { output: { layout: :hash } }
|
|
88
|
+
ruby = HaveAPI::ClientExamples::RubyClient.new('api.test', 'https://api.test/', '1', ['widget'], {}, 'show', action)
|
|
89
|
+
php = HaveAPI::ClientExamples::PhpClient.new('api.test', 'https://api.test/', '1', ['widget'], {}, 'show', action)
|
|
90
|
+
expect(ruby.example(sample)).to start_with('reply = api.widget.show(7,')
|
|
91
|
+
expect(php.example(sample)).to start_with('$reply = $api->widget->show(7,')
|
|
92
|
+
expect(ruby.example(sample)).not_to include('HaveAPI::Client.new')
|
|
93
|
+
expect(php.example(sample)).not_to include('new \\HaveAPI\\Client')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'offers callback setup once while preserving real action lookup and reply error handling' do
|
|
97
|
+
[HaveAPI::ClientExamples::JsClient, HaveAPI::ClientExamples::TsClient].each do |renderer|
|
|
98
|
+
context = dynamic_context
|
|
99
|
+
%i[init auth].each do |kind|
|
|
100
|
+
result = renderer.render(kind, context:, auth_method: :basic, auth_description: {})
|
|
101
|
+
expect(result.fetch(:code)).to include('if (!ready)', 'Discovery or authentication failed')
|
|
102
|
+
levels = renderer.detail_levels_for(kind, context:, result:, auth_method: :basic, auth_description: {})
|
|
103
|
+
expect(levels).to eq([:simple])
|
|
104
|
+
expect(levels).to be_frozen
|
|
105
|
+
end
|
|
106
|
+
result = renderer.render(:example, context:)
|
|
107
|
+
expect(result.fetch(:code)).to include('findAction', 'if (!reply.isOk())')
|
|
108
|
+
thorough = renderer.render(:example, context: context.with(detail: :thorough)).fetch(:code)
|
|
109
|
+
expect(thorough).to include('try {', 'catch (error)', 'reply.meta()')
|
|
110
|
+
expect(renderer.detail_levels_for(:example, context:, result:)).to eq(%i[simple thorough])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'offers async discovery and Python context-manager error handling for setup and actions' do
|
|
115
|
+
[HaveAPI::ClientExamples::JsAsyncClient, HaveAPI::ClientExamples::TsAsyncClient,
|
|
116
|
+
HaveAPI::ClientExamples::PythonClient, HaveAPI::ClientExamples::PythonAsyncClient].each do |renderer|
|
|
117
|
+
context = dynamic_context
|
|
118
|
+
%i[init auth example].each do |kind|
|
|
119
|
+
result = renderer.render(kind, context:, auth_method: :basic, auth_description: {})
|
|
120
|
+
levels = renderer.detail_levels_for(kind, context:, result:, auth_method: :basic, auth_description: {})
|
|
121
|
+
expect(levels).to eq(%i[simple thorough])
|
|
122
|
+
expect(levels).to be_frozen
|
|
123
|
+
thorough = renderer.render(kind, context: context.with(detail: :thorough),
|
|
124
|
+
auth_method: :basic, auth_description: {}).fetch(:code)
|
|
125
|
+
expect(thorough).to include(renderer.code == :python ? 'except HaveAPIError' : 'error instanceof AsyncError')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'offers Ruby and PHP real error handling while retaining simple direct calls' do
|
|
131
|
+
context = dynamic_context(resource_path: ['widget'], action_name: 'show')
|
|
132
|
+
[HaveAPI::ClientExamples::RubyClient, HaveAPI::ClientExamples::PhpClient].each do |renderer|
|
|
133
|
+
%i[init auth example].each do |kind|
|
|
134
|
+
result = renderer.render(kind, context:, auth_method: :basic, auth_description: {})
|
|
135
|
+
expect(renderer.detail_levels_for(kind, context:, result:, auth_method: :basic)).to eq(%i[simple thorough])
|
|
136
|
+
source = renderer.render(kind, context: context.with(detail: :thorough),
|
|
137
|
+
auth_method: :basic, auth_description: {}).fetch(:code)
|
|
138
|
+
expect(source).to include(renderer.code == :ruby ? 'api.setup' : '$api->setup()')
|
|
139
|
+
expect(source).to include('ValidationError', 'ActionFailed', renderer.code == :ruby ? 'rescue' : 'catch')
|
|
140
|
+
end
|
|
141
|
+
expect(context.detail).to eq(:simple)
|
|
142
|
+
expect(context.sample).to eq(dynamic_sample)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'advertises only Simple for built-in limitations without probing another render' do
|
|
147
|
+
context = dynamic_context(sample: dynamic_sample.merge(path_params: [7]))
|
|
148
|
+
results = renderers.to_h { |renderer| [renderer, renderer.render(:example, context:)] }
|
|
149
|
+
renderers.each do |renderer|
|
|
150
|
+
result = results.fetch(renderer)
|
|
151
|
+
expect(result).to include(status: :limitation)
|
|
152
|
+
allow(renderer).to receive(:render).and_call_original
|
|
153
|
+
expect(renderer.detail_levels_for(:example, context:, result:)).to eq([:simple])
|
|
154
|
+
expect(renderer).not_to have_received(:render)
|
|
155
|
+
end
|
|
156
|
+
[HaveAPI::ClientExamples::RubyClient, HaveAPI::ClientExamples::RubyCli].each do |renderer|
|
|
157
|
+
result = renderer.render(:auth, context:, auth_method: :oauth2, auth_description: {})
|
|
158
|
+
expect(result.fetch(:code)).to include('not supported')
|
|
159
|
+
expect(renderer.detail_levels_for(:auth, context:, result:, auth_method: :oauth2,
|
|
160
|
+
auth_description: {})).to eq([:simple])
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'authenticates protected Ruby and PHP programs with advertised literal existing credentials' do
|
|
165
|
+
context = dynamic_context(action: dynamic_action.merge(auth: true), resource_path: ['widget'], action_name: 'show', detail: :thorough)
|
|
166
|
+
[HaveAPI::ClientExamples::RubyClient, HaveAPI::ClientExamples::PhpClient].each do |renderer|
|
|
167
|
+
result = renderer.render(:example, context:)
|
|
168
|
+
expect(result[:status]).to eq(:code)
|
|
169
|
+
expect(result[:code]).to include('example-token')
|
|
170
|
+
expect(result[:code].index('example-token')).to be < result[:code].index(renderer.code == :ruby ? 'reply = api.widget' : '$reply = $api->widget')
|
|
171
|
+
unsupported = renderer.render(:example, context: context.with(description: { authentication: {} }))
|
|
172
|
+
expect(unsupported).to include(status: :limitation)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Dynamic example verification
|
|
2
2
|
|
|
3
3
|
The six JavaScript, TypeScript and Python variants are ordinary server renderers.
|
|
4
|
-
They require no
|
|
5
|
-
mounted base URLs and explicit API versions. Credentials
|
|
6
|
-
|
|
4
|
+
They require no API client profile. Their examples use exact public lookups,
|
|
5
|
+
mounted base URLs and explicit API versions. Credentials are public example
|
|
6
|
+
literals: `user`, `secret`, `example-token` and `example-access-token`. Examples
|
|
7
|
+
do not execute during a documentation request.
|
|
7
8
|
|
|
8
9
|
Run the focused checks in the prepared top-level Nix environment:
|
|
9
10
|
|
|
@@ -16,9 +17,11 @@ bundle exec rspec spec/client_examples/dynamic_js_spec.rb \
|
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
The unit checks parse the actual generated source and compile both TypeScript
|
|
19
|
-
variants against emitted public declarations.
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
variants at both detail levels against emitted public declarations. Simple
|
|
21
|
+
action fragments receive a declared initialized `api` in the compiler fixture;
|
|
22
|
+
Python async fragments are parsed in an async function. Prepare the JavaScript
|
|
23
|
+
build and its declared development dependencies first. The Rack fixture check
|
|
24
|
+
renders real mounted HTML and verifies extraction and request/result fixtures;
|
|
22
25
|
it opens no listening socket and does not install packages.
|
|
23
26
|
|
|
24
27
|
`make test-documentation-python` runs the supplemental native Python request
|
|
@@ -31,12 +34,11 @@ transport. It installs nothing and is separate from ordinary Ruby spec discovery
|
|
|
31
34
|
The focused JavaScript specs cross-check the built callback and async query/body
|
|
32
35
|
serializers with a mocked transport. The full `make test` includes both checks.
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
object supplies the continuation token.
|
|
37
|
+
Token-login examples use advertised typed literal input maps. Python selects
|
|
38
|
+
continuations through `step.next_action` and calls `step.submit(values)`;
|
|
39
|
+
JavaScript/TypeScript use their actual action callback argument. The runtime
|
|
40
|
+
propagates intermediate tokens. The acceptance fixture requires fixed lifetime,
|
|
41
|
+
credential request and verification before authenticated discovery.
|
|
40
42
|
|
|
41
43
|
`dynamic_acceptance.rb` checks the installed packages. Supply the built npm and
|
|
42
44
|
Python artifacts, the compiler and Node declaration directories, and a directory
|
|
@@ -54,20 +56,51 @@ bundle exec ruby spec/client_examples/dynamic_acceptance.rb \
|
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Repeat with the retained Python sdist and a separate evidence directory. Replace
|
|
57
|
-
artifact versions with the release being tested. The
|
|
58
|
-
packages without checkout imports
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
artifact versions with the release being tested. The harness installs the
|
|
60
|
+
supplied packages without checkout imports and starts its disposable mounted
|
|
61
|
+
fixture. It extracts examples from the overview, authentication and selected
|
|
62
|
+
nested-resource pages, requesting thorough detail. Each group's escaped
|
|
63
|
+
`data-example-details` map selects thorough when declared and Simple otherwise.
|
|
64
|
+
Missing or malformed declarations, duplicate panels, limitations and missing
|
|
65
|
+
code fail the run.
|
|
66
|
+
|
|
67
|
+
The fixture selects exactly 36 panels: six variants, each with initialization,
|
|
68
|
+
Basic, token, OAuth2, successful action and failed action. Eight callback
|
|
69
|
+
JavaScript/TypeScript initialization and provider panels are Simple; the other
|
|
70
|
+
28 are thorough. The harness compiles TypeScript with strict checking and runs
|
|
71
|
+
the exact extracted code with the installed clients. It preserves source,
|
|
72
|
+
including its stdout messages. This does not establish complete-program
|
|
73
|
+
acceptance of all Simple action fragments.
|
|
74
|
+
|
|
75
|
+
Checks cover discovery, Basic/existing token/OAuth credentials, a deterministic
|
|
76
|
+
token challenge, success/failure and request receipts. Receipts preserve exact
|
|
77
|
+
names, path order, Unicode, nested Custom values, omission, null, empty strings,
|
|
78
|
+
false and zero. Output and extracted programs remain in the evidence directory.
|
|
79
|
+
The fixture stops and the disposable consumer is removed when the operation
|
|
80
|
+
exits.
|
|
66
81
|
|
|
67
82
|
Each snippet's output is saved before completion checks, including nonzero
|
|
68
|
-
exits.
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
exits. Each of the 36 programs has its own before/after receipt snapshot and
|
|
84
|
+
saves its action/event delta beside the log. The six token-auth programs each
|
|
85
|
+
complete their challenge flow in that run. The fixture receipt endpoint returns
|
|
86
|
+
separate `actions` and `events` collections. Events contain only method, path,
|
|
87
|
+
status, provider/authenticated state and token request/verification completion.
|
|
88
|
+
They exclude credentials, tokens, challenge values, headers, queries and extra
|
|
89
|
+
request bodies. `fixture.log` records method, path and status.
|
|
90
|
+
|
|
91
|
+
Simple callback initialization and authentication code omits success-only
|
|
92
|
+
stdout markers. Completion requires successful mounted/versioned OPTIONS
|
|
93
|
+
discovery, authenticated through the expected provider where applicable.
|
|
94
|
+
Anonymous discovery alone is insufficient for authentication examples. Each
|
|
95
|
+
token program requires ordered request, verification and authenticated-discovery
|
|
96
|
+
events from that run. The twelve protected action programs require existing-token
|
|
97
|
+
authentication and discovery; challenge events cannot substitute for that proof.
|
|
98
|
+
|
|
99
|
+
The harness requires stdout markers where the selected source emits them, and
|
|
100
|
+
rejects failed callbacks or nonzero processes. Action samples retain exactly
|
|
101
|
+
one receipt, exact path/input values and DOC_SUCCESS/DOC_FAILURE.
|
|
102
|
+
`result.json` records group, variant and panel IDs with requested/effective
|
|
103
|
+
detail for all 36 programs, including `challenge_completions: 6`.
|
|
71
104
|
|
|
72
105
|
JavaScript list input and nested GET/OPTIONS query values produce a visible
|
|
73
106
|
limitation; HEAD input retains the client's JSON body behavior. Python GET
|
|
@@ -79,5 +112,5 @@ methods produce a limitation. Required global or object input metadata also
|
|
|
79
112
|
produces a limitation because the sample format has no metadata input channel.
|
|
80
113
|
Optional metadata needs no invented values. Prose-only and incomplete samples do
|
|
81
114
|
not become fabricated calls. Callback examples print the raw response; async
|
|
82
|
-
examples use the actual invocation receipt. Operation waiting
|
|
83
|
-
API
|
|
115
|
+
examples use the actual invocation receipt. Operation waiting uses the client's
|
|
116
|
+
existing API; a timeout does not cancel the server operation.
|
|
@@ -11,6 +11,7 @@ require_relative '../../test_support/dynamic_documentation_api'
|
|
|
11
11
|
|
|
12
12
|
module DynamicDocumentationAcceptance
|
|
13
13
|
VARIANTS = %w[javascript-callback javascript-async typescript-callback typescript-async python-sync python-async].freeze
|
|
14
|
+
PAGES = { overview: '', authentication: '?page=authentication', resource: '?resource=constructor.resources' }.freeze
|
|
14
15
|
|
|
15
16
|
def self.run!(*command, chdir:, env: {}, log_path: nil)
|
|
16
17
|
output, status = Open3.capture2e(env, *command, chdir:)
|
|
@@ -28,12 +29,150 @@ module DynamicDocumentationAcceptance
|
|
|
28
29
|
response.body
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
variant = VARIANTS.find { |name| id.end_with?("-#{name}") }
|
|
34
|
-
next unless variant
|
|
32
|
+
GROUPS = %w[init-examples auth-basic-examples auth-token-examples auth-oauth2-examples
|
|
33
|
+
example-constructor.resources-new-0-examples example-constructor.resources-new-1-examples].freeze
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
class DetailMap < Hash
|
|
36
|
+
def []=(key, value)
|
|
37
|
+
raise "Duplicate example detail declaration: #{key}" if has_key?(key)
|
|
38
|
+
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.attributes(source)
|
|
44
|
+
entries = source.scan(/([a-z][a-z0-9-]*)="([^"]*)"/)
|
|
45
|
+
raise 'Duplicate example HTML attribute' unless entries.map(&:first).uniq.length == entries.length
|
|
46
|
+
|
|
47
|
+
entries.to_h.transform_values { |value| CGI.unescapeHTML(value) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.extract(html, detail: :thorough)
|
|
51
|
+
raise 'Invalid requested example detail' unless %i[simple thorough].include?(detail)
|
|
52
|
+
|
|
53
|
+
groups = html.scan(%r{<div\b([^>]*\bdata-example(?=\s|>)[^>]*)>(.*?)</div>}m)
|
|
54
|
+
raise 'Malformed example group' unless groups.length == html.scan(/<div\b[^>]*\bdata-example(?=\s|>)/).length
|
|
55
|
+
|
|
56
|
+
ids = []
|
|
57
|
+
groups.flat_map do |attributes, body|
|
|
58
|
+
group = self.attributes(attributes)
|
|
59
|
+
id = group.fetch('id')
|
|
60
|
+
raise "Duplicate example group: #{id}" if ids.include?(id)
|
|
61
|
+
|
|
62
|
+
ids << id
|
|
63
|
+
declarations = JSON.parse(group.fetch('data-example-details'), object_class: DetailMap)
|
|
64
|
+
unless declarations.is_a?(Hash) && declarations.all? do |client, levels|
|
|
65
|
+
client.is_a?(String) && [%w[simple], %w[simple thorough]].include?(levels)
|
|
66
|
+
end
|
|
67
|
+
raise "Invalid example detail declarations: #{id}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
actual = body.scan(%r{<details\b([^>]*\bdata-example-panel\b[^>]*)>(.*?)</details>}m)
|
|
71
|
+
VARIANTS.map do |variant|
|
|
72
|
+
levels = declarations.fetch(variant)
|
|
73
|
+
effective = levels.include?(detail.to_s) ? detail.to_s : 'simple'
|
|
74
|
+
variants = actual.select { |data, _code| self.attributes(data)['data-client'] == variant }
|
|
75
|
+
unless variants.map { |data, _code| self.attributes(data)['data-detail'] }.sort == levels.sort
|
|
76
|
+
raise "Missing, duplicate or undeclared example panels: #{id}/#{variant}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
data, code_body = variants.find { |data, _code| self.attributes(data)['data-detail'] == effective }
|
|
80
|
+
panel = self.attributes(data)
|
|
81
|
+
code = code_body.scan(%r{<pre><code class="([^"]+)">(.*?)</code></pre>}m)
|
|
82
|
+
unless code.length == 1 && code[0][0] == variant.split('-').first && !code_body.include?('class="client-limitation"')
|
|
83
|
+
raise "Expected one code panel: #{id}/#{variant}/#{effective}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
{ group: id, id: panel.fetch('id'), variant:, language: code[0][0], requested_detail: detail,
|
|
87
|
+
detail: effective.to_sym, source: CGI.unescapeHTML(code[0][1]) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.validate_coverage!(panels)
|
|
93
|
+
VARIANTS.each do |variant|
|
|
94
|
+
selected = panels.select { |panel| panel[:variant] == variant }
|
|
95
|
+
raise "Incomplete rendered coverage for #{variant}" unless selected.map { |panel| panel[:group] }.sort == GROUPS.sort
|
|
96
|
+
|
|
97
|
+
selected.each do |panel|
|
|
98
|
+
singleton = %w[javascript-callback typescript-callback].include?(variant) && !panel[:group].start_with?('example-')
|
|
99
|
+
expected = singleton ? :simple : :thorough
|
|
100
|
+
unless panel[:requested_detail] == :thorough && panel[:detail] == expected
|
|
101
|
+
raise "Unexpected effective example detail: #{panel[:group]}/#{variant}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
unless panels.length == 36 && panels.count { |panel| panel[:detail] == :simple } == 8 &&
|
|
106
|
+
panels.map { |panel| panel[:id] }.uniq.length == panels.length
|
|
107
|
+
raise 'Unexpected rendered panel matrix'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.receipt_delta(before, after)
|
|
112
|
+
unless [before, after].all? { |receipt| receipt.is_a?(Hash) && receipt.keys.sort == %w[actions events] && receipt.values.all?(Array) }
|
|
113
|
+
raise 'Invalid fixture receipt envelope'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
before.to_h do |kind, previous|
|
|
117
|
+
current = after.fetch(kind)
|
|
118
|
+
raise 'Fixture receipts changed before the run boundary' unless current.take(previous.length) == previous
|
|
119
|
+
|
|
120
|
+
[kind, current.drop(previous.length)]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.authenticated_discovery(events, provider)
|
|
125
|
+
events.each_index.find do |index|
|
|
126
|
+
event = events[index]
|
|
127
|
+
request = event['type'] == 'request' && event['method'] == 'OPTIONS' && event['path'] == '/mount/v2/' &&
|
|
128
|
+
event['status'] == 200 && event['provider'] == provider && event['authenticated'] == !provider.nil?
|
|
129
|
+
request && (provider.nil? || events.take(index).any? do |previous|
|
|
130
|
+
previous['type'] == 'authentication' && previous['provider'] == provider && previous['authenticated'] == true
|
|
131
|
+
end)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.verify_completion!(panel, output, delta)
|
|
136
|
+
raise "Failed callback: #{panel[:id]}" if output.include?('Discovery or authentication failed')
|
|
137
|
+
|
|
138
|
+
actions = delta.fetch('actions')
|
|
139
|
+
events = delta.fetch('events')
|
|
140
|
+
if panel[:group].start_with?('example-')
|
|
141
|
+
raise "Rendered action did not execute once: #{panel[:id]}" unless actions.length == 1
|
|
142
|
+
|
|
143
|
+
receipt = actions.first
|
|
144
|
+
expected = { 'name' => '', 'enabled' => false, 'nullable' => nil, 'payload' => DynamicDocumentationAPI::PAYLOAD }
|
|
145
|
+
raise "Changed request values: #{receipt.inspect}" unless receipt['input'].except('count') == expected
|
|
146
|
+
raise 'Changed mounted path arguments' unless receipt['path'].map(&:to_s) == ['7', 'name with space']
|
|
147
|
+
|
|
148
|
+
count = panel[:group] == GROUPS.last ? -1 : 0
|
|
149
|
+
raise 'Changed sample success/failure input' unless receipt['input']['count'] == count
|
|
150
|
+
|
|
151
|
+
unless authenticated_discovery(events, 'token') && events.none? { |event| event['type'].start_with?('token_') }
|
|
152
|
+
raise "Existing-token action authentication did not complete: #{panel[:id]}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
marker = count < 0 ? 'DOC_FAILURE' : 'DOC_SUCCESS'
|
|
156
|
+
raise "Wrong result/error handling: #{panel[:id]}\n#{output}" unless output.include?(marker)
|
|
157
|
+
else
|
|
158
|
+
raise "Unexpected action during setup: #{panel[:id]}" unless actions.empty?
|
|
159
|
+
|
|
160
|
+
provider = panel[:group] == 'init-examples' ? nil : panel[:group].delete_prefix('auth-').delete_suffix('-examples')
|
|
161
|
+
discovery = authenticated_discovery(events, provider)
|
|
162
|
+
raise "Discovery/authentication did not complete: #{panel[:id]}" unless discovery
|
|
163
|
+
|
|
164
|
+
if provider == 'token'
|
|
165
|
+
sequence = events.map { |event| event['type'] }
|
|
166
|
+
request = sequence.index('token_request')
|
|
167
|
+
verification = sequence.index('token_verification')
|
|
168
|
+
unless provider == 'token' && request && verification && request < verification && verification < discovery
|
|
169
|
+
raise "Token challenge did not complete: #{panel[:id]}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
if panel[:detail] == :thorough
|
|
173
|
+
marker = provider ? 'Authenticated' : 'Discovery complete'
|
|
174
|
+
raise "Missing completion output: #{panel[:id]}\n#{output}" unless output.include?(marker)
|
|
175
|
+
end
|
|
37
176
|
end
|
|
38
177
|
end
|
|
39
178
|
|
|
@@ -63,31 +202,20 @@ module DynamicDocumentationAcceptance
|
|
|
63
202
|
end
|
|
64
203
|
end
|
|
65
204
|
reader = Thread.new { File.write(File.join(output, 'fixture.log'), server_output.read) }
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
VARIANTS.each do |variant|
|
|
72
|
-
selected = panels.select { |panel| panel[:variant] == variant }
|
|
73
|
-
raise "Incomplete rendered coverage for #{variant}" unless selected.length >= 4 &&
|
|
74
|
-
selected.any? { |panel| panel[:id].start_with?('init-') } &&
|
|
75
|
-
selected.any? { |panel| panel[:id].start_with?('auth-basic-') } &&
|
|
76
|
-
selected.count { |panel| panel[:id].start_with?('example-') } >= 2
|
|
205
|
+
panels = PAGES.flat_map do |page, query|
|
|
206
|
+
html = request("#{base}/mount/v2/#{query}")
|
|
207
|
+
File.write(File.join(output, "documentation-#{page}.html"), html)
|
|
208
|
+
extract(html)
|
|
77
209
|
end
|
|
210
|
+
validate_coverage!(panels)
|
|
211
|
+
executions = []
|
|
78
212
|
panels.each_with_index do |panel, index|
|
|
79
213
|
extension = { 'typescript' => '.ts', 'javascript' => '.mjs', 'python' => '.py' }.fetch(panel[:language])
|
|
80
214
|
name = "example-#{index}#{extension}"
|
|
81
215
|
path = File.join(consumer, name)
|
|
82
216
|
File.write(path, panel[:source])
|
|
83
217
|
File.write(File.join(output, name), panel[:source])
|
|
84
|
-
env = { 'NODE_PATH' => nil, 'PYTHONPATH' => nil, 'PYTHONHOME' => nil
|
|
85
|
-
'HAVEAPI_USER' => 'fixture-user', 'HAVEAPI_PASSWORD' => 'fixture-password',
|
|
86
|
-
'HAVEAPI_TOKEN' => 'fixture-token', 'HAVEAPI_ACCESS_TOKEN' => 'fixture-access-token',
|
|
87
|
-
'HAVEAPI_AUTH_user' => JSON.generate('fixture-user'),
|
|
88
|
-
'HAVEAPI_AUTH_password' => JSON.generate('fixture-password'),
|
|
89
|
-
'HAVEAPI_AUTH_lifetime' => JSON.generate('fixed'),
|
|
90
|
-
'HAVEAPI_AUTH_scope' => JSON.generate('all'), 'HAVEAPI_CHALLENGE_code' => JSON.generate('123456') }
|
|
218
|
+
env = { 'NODE_PATH' => nil, 'PYTHONPATH' => nil, 'PYTHONHOME' => nil }
|
|
91
219
|
if extension == '.ts'
|
|
92
220
|
config = {
|
|
93
221
|
compilerOptions: { target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', strict: true,
|
|
@@ -104,29 +232,21 @@ module DynamicDocumentationAcceptance
|
|
|
104
232
|
end
|
|
105
233
|
before = JSON.parse(request("#{base}/__acceptance_receipts"))
|
|
106
234
|
command = panel[:language] == 'python' ? [python, path] : ['node', path]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
after = JSON.parse(request("#{base}/__acceptance_receipts"))
|
|
114
|
-
if panel[:id].start_with?('example-')
|
|
115
|
-
raise "Rendered action did not execute: #{panel[:id]}\n#{result}" unless after.length == before.length + 1
|
|
116
|
-
|
|
117
|
-
receipt = after.last
|
|
118
|
-
expected = { 'name' => '', 'enabled' => false, 'nullable' => nil, 'payload' => DynamicDocumentationAPI::PAYLOAD }
|
|
119
|
-
raise "Changed request values: #{receipt.inspect}" unless receipt['input'].except('count') == expected
|
|
120
|
-
raise 'Changed mounted path arguments' unless receipt['path'].map(&:to_s) == ['7', 'name with space']
|
|
121
|
-
|
|
122
|
-
marker = receipt['input']['count'] < 0 ? 'DOC_FAILURE' : 'DOC_SUCCESS'
|
|
123
|
-
raise "Wrong result/error handling: #{panel[:id]}\n#{result}" unless result.include?(marker)
|
|
124
|
-
else
|
|
125
|
-
marker = panel[:id].start_with?('init-') ? 'Discovery complete' : 'Authenticated'
|
|
126
|
-
raise "Discovery/authentication did not complete: #{panel[:id]}\n#{result}" unless result.include?(marker)
|
|
235
|
+
begin
|
|
236
|
+
result = run!(*command, chdir: consumer, env:, log_path: File.join(output, "example-#{index}.log"))
|
|
237
|
+
ensure
|
|
238
|
+
after = JSON.parse(request("#{base}/__acceptance_receipts"))
|
|
239
|
+
delta = receipt_delta(before, after)
|
|
240
|
+
File.write(File.join(output, "example-#{index}-receipts.json"), JSON.pretty_generate(delta))
|
|
127
241
|
end
|
|
242
|
+
verify_completion!(panel, result, delta)
|
|
243
|
+
executions << panel.except(:source).merge(challenge_completion: panel[:group] == 'auth-token-examples')
|
|
128
244
|
end
|
|
129
|
-
|
|
245
|
+
raise 'Incomplete token challenge coverage' unless executions.count { |execution| execution[:challenge_completion] } == VARIANTS.length
|
|
246
|
+
|
|
247
|
+
File.write(File.join(output, 'result.json'), JSON.pretty_generate({ panels: panels.length, variants: VARIANTS,
|
|
248
|
+
details: panels.group_by { |panel| panel[:detail] }.transform_values(&:length),
|
|
249
|
+
challenge_completions: VARIANTS.length, executions:, status: 'passed' }))
|
|
130
250
|
ensure
|
|
131
251
|
Process.kill('TERM', wait.pid) if wait.alive?
|
|
132
252
|
wait.value
|