haveapi-go-client 0.29.12 → 0.30.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.
@@ -0,0 +1,271 @@
1
+ # Run after committed-deliverable review; this generates, compiles and executes SDKs.
2
+ require 'json'
3
+ require 'open3'
4
+ require 'tmpdir'
5
+ require 'tempfile'
6
+ require 'webrick'
7
+
8
+ require 'haveapi/go_client/documentation'
9
+ require 'haveapi/documentation_clients'
10
+ require 'haveapi/client_examples/go_client'
11
+ require_relative 'support/documentation_fixture'
12
+
13
+ class GoDocumentationAcceptance
14
+ def self.build_command(destination)
15
+ # The disposable consumer has no source history to stamp into its binaries.
16
+ ['go', 'build', '-buildvcs=false', '-o', destination, './cmd/...']
17
+ end
18
+
19
+ def self.expected_cases
20
+ # The SDK emits an empty metadata envelope for this metadata-bearing action.
21
+ object_body = { 'widget' => { 'name' => nil, 'enabled' => false, 'count' => 0 }, '_meta' => {} }
22
+ {
23
+ init: {}, basic: {}, token: {}, oauth2: {},
24
+ object: { body: object_body, output: '123' },
25
+ empty: { body: { 'widget' => {} } }, omitted: { body: { 'widget' => {} } },
26
+ list: { output: "1\n2" }, fail: { output: 'fixture failure' },
27
+ association: { output: '9' },
28
+ # The SDK encodes false/true query booleans as 0/1.
29
+ query: { query: { 'widget[name]' => '', 'widget[enabled]' => '0', 'widget[count]' => '0' } },
30
+ operation: { state: true },
31
+ basic_call: { body: object_body, auth: { authorization: "Basic #{['fixture-user:fixture-password'].pack('m0')}" } },
32
+ token_call: { body: object_body, auth: { token: 'fixture-token' } },
33
+ oauth2_call: { body: object_body, auth: { oauth2: 'fixture-access-token' } },
34
+ hostile: { body: { 'widget' => { 'name' => "quote\"\\\n雪</script>" }, '_meta' => {} }, output: '123' }
35
+ }
36
+ end
37
+
38
+ def self.run!
39
+ check_environment!
40
+ new.run!
41
+ end
42
+
43
+ # The same optional gems must be visible to Bundler and the product loader.
44
+ # This preflight performs no generation, compilation or server startup.
45
+ def self.check_environment!
46
+ %w[haveapi haveapi-go-client].each do |name|
47
+ raise "#{name} is absent from the acceptance bundle" unless Gem.loaded_specs.has_key?(name)
48
+ end
49
+ unless Gem.loaded_specs.fetch('haveapi-go-client').version.to_s == HaveAPI::GoClient::VERSION
50
+ raise 'activated Go generator specification does not match its loaded version'
51
+ end
52
+
53
+ Tempfile.create(['haveapi-go-documentation-profile-', '.json']) do |file|
54
+ file.write(JSON.generate(GoDocumentationFixture.description))
55
+ file.flush
56
+ profiles = HaveAPI::ClientExample::Profiles.new
57
+ profiles.register('7', go: {
58
+ import_path: 'example.test/sdk/client', module_version: 'v1.2.3', description_path: file.path,
59
+ generator_version: Gem.loaded_specs.fetch('haveapi-go-client').version.to_s
60
+ })
61
+ profiles.mount(['7'])
62
+ raise 'configured Go profile is unavailable' unless profiles.for_version('7').fetch(:go).language == :go
63
+ end
64
+ end
65
+
66
+ def initialize
67
+ @requests = Queue.new
68
+ @server = WEBrick::HTTPServer.new(
69
+ BindAddress: '127.0.0.1', Port: 0, AccessLog: [], Logger: WEBrick::Log.new(File::NULL)
70
+ )
71
+ @server.mount_proc('/') { |request, response| serve(request, response) }
72
+ end
73
+
74
+ def run!
75
+ thread = Thread.new { @server.start }
76
+ Dir.mktmpdir('haveapi-go-documentation-') do |dir|
77
+ prepare_sdk(dir)
78
+ prepare_programs(dir)
79
+ command('gofmt', '-w', File.join(dir, 'consumer'))
80
+ command(*self.class.build_command(File.join(dir, 'bin')), chdir: File.join(dir, 'consumer'))
81
+ @cases.each do |name, expected|
82
+ stdout = command(File.join(dir, 'bin', name.to_s), env: {
83
+ 'HAVEAPI_USER' => 'fixture-user', 'HAVEAPI_PASSWORD' => 'fixture-password',
84
+ 'HAVEAPI_TOKEN' => 'fixture-token', 'HAVEAPI_ACCESS_TOKEN' => 'fixture-access-token'
85
+ })
86
+ verify(name, expected, stdout)
87
+ end
88
+ end
89
+ # This is a standalone acceptance runner, not an RSpec example.
90
+ $stdout.write("Go documentation acceptance: #{@cases.length} rendered programs compiled and executed\n") # rubocop:disable RSpec/Output
91
+ ensure
92
+ @server.shutdown
93
+ thread&.join
94
+ end
95
+
96
+ private
97
+
98
+ def prepare_sdk(dir)
99
+ @description = GoDocumentationFixture.description
100
+ @description[:resources][:client][:resources][:child][:actions].merge!(
101
+ empty: GoDocumentationFixture.action(path: '/mounted/v7/empty', input: input),
102
+ omitted: GoDocumentationFixture.action(path: '/mounted/v7/omitted', input: input),
103
+ list: GoDocumentationFixture.action(path: '/mounted/v7/list', method: 'GET', output: output(layout: 'hash_list')),
104
+ association: GoDocumentationFixture.action(
105
+ path: '/mounted/v7/association', method: 'GET',
106
+ output: GoDocumentationFixture.io({ owner: { type: 'Resource', resource: ['owner'], value_id: 'id', value_label: 'label' } })
107
+ ),
108
+ fail: GoDocumentationFixture.action(path: '/mounted/v7/fail', method: 'GET'),
109
+ query: GoDocumentationFixture.action(path: '/mounted/v7/query', method: 'GET', input: input),
110
+ operation: GoDocumentationFixture.action(
111
+ path: '/mounted/v7/operation', method: 'GET', blocking: true,
112
+ meta: { global: { output: GoDocumentationFixture.io({ action_state_id: { type: 'Integer' } }, namespace: false) } }
113
+ )
114
+ )
115
+ # This exact local JSON is both generator input and the configured profile.
116
+ description_path = File.join(dir, 'sdk-description.json')
117
+ File.write(description_path, JSON.pretty_generate(@description))
118
+ saved = JSON.parse(File.read(description_path), symbolize_names: true)
119
+ generator = HaveAPI::GoClient::Generator.from_description(saved, File.join(dir, 'sdk'),
120
+ module: 'example.test/sdk', package: 'client')
121
+ generator.generate
122
+ profiles = HaveAPI::ClientExample::Profiles.new
123
+ profiles.register('7', go: {
124
+ import_path: 'example.test/sdk/client', module_version: 'v1.2.3', description_path:,
125
+ generator_version: HaveAPI::GoClient::VERSION
126
+ })
127
+ profiles.mount(['7'])
128
+ @profile = profiles.for_version('7').fetch(:go)
129
+ FileUtils.mkdir_p(File.join(dir, 'consumer'))
130
+ FileUtils.mkdir_p(File.join(dir, 'bin'))
131
+ File.write(File.join(dir, 'consumer/go.mod'), <<~MOD)
132
+ module example.test/documentation-consumer
133
+
134
+ go 1.20
135
+
136
+ require example.test/sdk v1.2.3
137
+ replace example.test/sdk => ../sdk
138
+ MOD
139
+ end
140
+
141
+ def prepare_programs(dir)
142
+ @cases = self.class.expected_cases
143
+ @cases.each_key do |name|
144
+ kind = if name == :init
145
+ :init
146
+ else
147
+ (%i[basic token oauth2].include?(name) ? :auth : :example)
148
+ end
149
+ action_name = %i[object basic_call token_call oauth2_call hostile].include?(name) ? :client : name
150
+ action = @description.dig(:resources, :client, :resources, :child, :actions, action_name)
151
+ sample = case name
152
+ when :object, :basic_call, :token_call, :oauth2_call then GoDocumentationFixture.sample
153
+ when :hostile then GoDocumentationFixture.sample.merge(request: { name: "quote\"\\\n雪</script>" })
154
+ when :empty then { request: {}, response: {}, status: true }
155
+ when :omitted then { response: {}, status: true }
156
+ when :list then { response: [{ id: 1 }, { id: 2 }], status: true }
157
+ when :association then { response: { owner: { id: 9 } }, status: true }
158
+ when :fail then { status: false }
159
+ when :query then { request: { name: nil, enabled: false, count: 0 }, status: true }
160
+ else { status: true }
161
+ end
162
+ context = HaveAPI::ClientExample::Context.new(
163
+ version: '7', host: '127.0.0.1', base_url: "http://127.0.0.1:#{@server.config[:Port]}/mounted/",
164
+ version_url: "http://127.0.0.1:#{@server.config[:Port]}/mounted/v7/", description: @description,
165
+ resource_path: %w[client child], action_name:, action:, sample:, profile: @profile
166
+ )
167
+ result = HaveAPI::ClientExamples::GoClient.render(kind, context:, auth_method: name, auth_description: {})
168
+ raise "#{name}: unexpected limitation: #{result[:reason]}" unless result[:status] == :code
169
+
170
+ path = File.join(dir, 'consumer/cmd', name.to_s)
171
+ FileUtils.mkdir_p(path)
172
+ # The complete program is the actual renderer result, without replacements.
173
+ source = result.fetch(:code)
174
+ if name.to_s.end_with?('_call')
175
+ auth = HaveAPI::ClientExamples::GoClient.render(:auth, context:, auth_method: name.to_s.delete_suffix('_call').to_sym, auth_description: {})
176
+ raise "#{name}: authentication limitation" unless auth[:status] == :code
177
+
178
+ source = compose(auth.fetch(:code), source)
179
+ end
180
+ File.write(File.join(path, 'main.go'), source)
181
+ end
182
+ end
183
+
184
+ def input
185
+ GoDocumentationFixture.io({ name: { type: 'String', nullable: true }, enabled: { type: 'Boolean' },
186
+ count: { type: 'Integer' }, unused: { type: 'String' } })
187
+ end
188
+
189
+ def output(layout: 'hash')
190
+ GoDocumentationFixture.io({ id: { type: 'Integer' }, name: { type: 'String', nullable: true } }, layout:)
191
+ end
192
+
193
+ def serve(request, response)
194
+ @requests << { path: request.path, query: request.query, raw_query: request.query_string, body: request.body,
195
+ auth: { authorization: request['Authorization'], token: request['X-Token'], oauth2: request['X-OAuth'] } }
196
+ response['Content-Type'] = 'application/json'
197
+ result = case request.path
198
+ when '/mounted/v7/fail'
199
+ { status: false, message: 'fixture failure', errors: { name: ['invalid'] } }
200
+ when '/mounted/v7/list'
201
+ { status: true, response: { widget: [{ id: 1 }, { id: 2 }] } }
202
+ when '/mounted/v7/association'
203
+ { status: true, response: { widget: { owner: { id: 9 } } } }
204
+ when '/mounted/v7/operation'
205
+ { status: true, response: { _meta: { action_state_id: 55 } } }
206
+ when '/mounted/v7/action_states/55'
207
+ { status: true, response: { action_state: { id: 55 } } }
208
+ else
209
+ { status: true, response: { widget: { id: 123 } } }
210
+ end
211
+ response.body = JSON.generate(result)
212
+ end
213
+
214
+ def verify(name, expected, stdout)
215
+ if %i[init basic token oauth2].include?(name)
216
+ raise "#{name}: initialization performed a request" unless @requests.empty?
217
+
218
+ return
219
+ end
220
+
221
+ request = @requests.pop(true)
222
+ expected_path = %i[object basic_call token_call oauth2_call hostile].include?(name) ? '/mounted/v7/clients/42/children/a/b' : "/mounted/v7/#{name}"
223
+ raise "#{name}: unexpected mounted/versioned path #{request[:path]}" unless request[:path] == expected_path
224
+ if expected[:body] && JSON.parse(request[:body]) != expected[:body]
225
+ raise "#{name}: supplied input or omission was not preserved: #{request[:body]}"
226
+ end
227
+
228
+ if expected[:query] && request[:query] != expected[:query]
229
+ raise "#{name}: query values were not preserved: expected #{expected[:query].inspect}, " \
230
+ "got #{request[:query].inspect}; raw query #{request[:raw_query].inspect}"
231
+ end
232
+
233
+ if expected[:auth] && !expected[:auth].all? { |key, value| request[:auth][key] == value }
234
+ raise "#{name}: rendered authentication did not reach the fixture"
235
+ end
236
+ raise "#{name}: unexpected rendered output #{stdout.inspect}" if expected[:output] && !stdout.include?(expected[:output])
237
+
238
+ if expected[:state]
239
+ state = @requests.pop(true)
240
+ raise 'operation: generated helper used the wrong route' unless state[:path] == '/mounted/v7/action_states/55'
241
+ end
242
+ raise "#{name}: unexpected extra request" unless @requests.empty?
243
+ end
244
+
245
+ # Compose exact rendered sections, deduplicating their shared initialization.
246
+ def compose(auth, action)
247
+ imports = [auth, action].flat_map { |source| source.match(/^import \(\n(.*?)\n\)/m)[1].lines(chomp: true) }.uniq
248
+ auth_body = auth.match(/^func main\(\) \{\n(.*)\n\}\n/m)[1].lines
249
+ action_body = action.match(/^func main\(\) \{\n(.*)\n\}\n/m)[1].lines
250
+ raise 'rendered initialization differs across sections' unless auth_body.first == action_body.first
251
+
252
+ [auth.split('import (', 2).first, "import (\n", imports.join("\n"), "\n)\n\nfunc main() {\n",
253
+ auth_body.join, "\n", action_body.drop(1).join, "\n}\n"].join
254
+ end
255
+
256
+ def command(*args, chdir: nil, env: {})
257
+ environment = { 'GOWORK' => 'off', 'GOPROXY' => 'off', 'GOSUMDB' => 'off', 'GOTOOLCHAIN' => 'local' }.merge(env)
258
+ stdout, stderr, status = Open3.capture3(environment, *args, **(chdir ? { chdir: } : {}))
259
+ raise "#{args.first} failed: #{stdout}\n#{stderr}" unless status.success?
260
+
261
+ stdout
262
+ end
263
+ end
264
+
265
+ if $0 == __FILE__
266
+ case ARGV
267
+ when [] then GoDocumentationAcceptance.run!
268
+ when ['--check-environment'] then GoDocumentationAcceptance.check_environment!
269
+ else raise ArgumentError, 'usage: documentation_acceptance.rb [--check-environment]'
270
+ end
271
+ end
@@ -0,0 +1,31 @@
1
+ # Native dry-run regression: no compiler, listener or generated SDK is started.
2
+ require_relative 'documentation_acceptance'
3
+
4
+ class GoDocumentationBuildCheck
5
+ def self.run!
6
+ Dir.mktmpdir('haveapi-go-documentation-build-') do |root|
7
+ # Own this invalid ancestor marker; never rely on or alter ambient Git state.
8
+ Dir.mkdir(File.join(root, '.git'))
9
+ consumer = File.join(root, 'consumer')
10
+ FileUtils.mkdir_p(File.join(consumer, 'cmd/probe'))
11
+ File.write(File.join(consumer, 'go.mod'), "module example.test/documentation-consumer\n\ngo 1.20\n")
12
+ File.write(File.join(consumer, 'cmd/probe/main.go'), "package main\nfunc main() {}\n")
13
+ command = GoDocumentationAcceptance.build_command(File.join(root, 'bin')).insert(2, '-n', '-x')
14
+ environment = { 'GOWORK' => 'off', 'GOPROXY' => 'off', 'GOSUMDB' => 'off', 'GOTOOLCHAIN' => 'local' }
15
+ _output, error, status = Open3.capture3(environment, *command, chdir: consumer)
16
+ raise "documentation build plan failed: #{error}" unless status.success?
17
+ raise 'documentation build unexpectedly queried Git status' if error.include?('git status --porcelain')
18
+
19
+ # Prove the regression condition with the same command minus its policy flag.
20
+ control = command.reject { |argument| argument == '-buildvcs=false' }
21
+ _output, error, status = Open3.capture3(environment, *control, chdir: consumer)
22
+ unless !status.success? && error.include?('error obtaining VCS status') &&
23
+ error.include?("cd #{root}\n") && error.include?('git status --porcelain')
24
+ raise "unflagged control did not reproduce owned ancestor VCS discovery: #{error}"
25
+ end
26
+ end
27
+ $stdout.write("Go documentation build dry-run and unflagged VCS control passed\n") # rubocop:disable RSpec/Output
28
+ end
29
+ end
30
+
31
+ GoDocumentationBuildCheck.run! if $0 == __FILE__
@@ -0,0 +1,94 @@
1
+ # Pure verifier regression: no generator, compiler or listener is started.
2
+ require 'stringio'
3
+ require 'uri'
4
+ require_relative 'documentation_acceptance'
5
+
6
+ class GoDocumentationWireCheck
7
+ OBJECT_BODY = JSON.parse('{"widget":{"count":0,"enabled":false,"name":null},"_meta":{}}').freeze
8
+ OBJECT_CASES = %i[object basic_call token_call oauth2_call].freeze
9
+
10
+ def self.run!
11
+ OBJECT_CASES.each do |name|
12
+ verify(name, OBJECT_BODY)
13
+ reject_change(name) { |body| body['_meta'] = { 'includes' => 'unrequested' } }
14
+ reject_change(name) { |body| body['_meta'] = nil }
15
+ reject_change(name) { |body| body.delete('_meta') }
16
+ reject_change(name) { |body| body['extra'] = 'unrequested' }
17
+ reject_change(name) { |body| body['widget']['unused'] = '' }
18
+ reject_change(name) { |body| body['widget']['enabled'] = true }
19
+ reject_change(name) { |body| body['widget']['count'] = 1 }
20
+ reject_change(name) { |body| body['widget']['name'] = '' }
21
+ %w[name enabled count].each do |field|
22
+ reject_change(name) { |body| body['widget'].delete(field) }
23
+ end
24
+ end
25
+
26
+ hostile = { 'widget' => { 'name' => "quote\"\\\n雪</script>" }, '_meta' => {} }
27
+ verify(:hostile, hostile)
28
+ reject_change(:hostile, hostile) { |body| body['_meta']['includes'] = 'unrequested' }
29
+ reject_change(:hostile, hostile) { |body| body['widget']['unused'] = '' }
30
+
31
+ %i[empty omitted].each do |name|
32
+ body = { 'widget' => {} }
33
+ verify(name, body)
34
+ reject_change(name, body) { |changed| changed['_meta'] = {} }
35
+ reject_change(name, body) { |changed| changed['widget']['unused'] = '' }
36
+ reject_change(name, body) { |changed| changed.delete('widget') }
37
+ end
38
+ check_query
39
+ $stdout.write("Go documentation wire verifier: 7 accepted bodies and 52 rejected mutations\n") # rubocop:disable RSpec/Output
40
+ $stdout.write("Go documentation query verifier: 1 accepted query and 8 rejected mutations\n") # rubocop:disable RSpec/Output
41
+ end
42
+
43
+ def self.check_query
44
+ verify_query('widget%5Bcount%5D=0&widget%5Benabled%5D=0&widget%5Bname%5D=')
45
+ original = { 'widget[name]' => '', 'widget[enabled]' => '0', 'widget[count]' => '0' }
46
+ [
47
+ original.merge('widget[enabled]' => 'false'),
48
+ original.merge('widget[enabled]' => '1'),
49
+ original.merge('widget[count]' => '1'),
50
+ original.merge('widget[name]' => 'null'),
51
+ *original.keys.map { |key| original.reject { |name, _value| name == key } },
52
+ original.merge('widget[unused]' => '')
53
+ ].each { |query| reject_query(URI.encode_www_form(query)) }
54
+ end
55
+
56
+ def self.verify_query(raw_query)
57
+ request = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
58
+ request.parse(StringIO.new("GET /mounted/v7/query?#{raw_query} HTTP/1.1\r\nHost: fixture.invalid\r\n\r\n"))
59
+ response = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP)
60
+ runner = GoDocumentationAcceptance.allocate
61
+ runner.instance_variable_set(:@requests, Queue.new)
62
+ runner.send(:serve, request, response)
63
+ runner.send(:verify, :query, GoDocumentationAcceptance.expected_cases.fetch(:query), '')
64
+ end
65
+
66
+ def self.reject_query(raw_query)
67
+ verify_query(raw_query)
68
+ raise 'query: verifier accepted an unexpected query'
69
+ rescue RuntimeError => e
70
+ raise unless e.message.start_with?('query: query values were not preserved: expected ') &&
71
+ e.message.include?('got ') && e.message.include?("raw query #{raw_query.inspect}")
72
+ end
73
+
74
+ def self.verify(name, body)
75
+ expected = GoDocumentationAcceptance.expected_cases.fetch(name)
76
+ runner = GoDocumentationAcceptance.allocate
77
+ requests = Queue.new
78
+ path = %i[empty omitted].include?(name) ? "/mounted/v7/#{name}" : '/mounted/v7/clients/42/children/a/b'
79
+ requests << { path:, body: JSON.generate(body), auth: expected.fetch(:auth, {}) }
80
+ runner.instance_variable_set(:@requests, requests)
81
+ runner.send(:verify, name, expected, expected.fetch(:output, ''))
82
+ end
83
+
84
+ def self.reject_change(name, original = OBJECT_BODY)
85
+ body = Marshal.load(Marshal.dump(original))
86
+ yield(body)
87
+ verify(name, body)
88
+ raise "#{name}: verifier accepted an unexpected body"
89
+ rescue RuntimeError => e
90
+ raise unless e.message.start_with?("#{name}: supplied input or omission was not preserved:")
91
+ end
92
+ end
93
+
94
+ GoDocumentationWireCheck.run! if $0 == __FILE__
@@ -0,0 +1,70 @@
1
+ # Saved SDK schema fixture. Generated Go source is always produced by Generator.
2
+ module GoDocumentationFixture
3
+ def self.io(parameters, layout: 'hash', namespace: 'widget')
4
+ { layout:, namespace:, parameters: }
5
+ end
6
+
7
+ def self.action(path: '/mounted/v7/widgets', method: 'POST', input: nil, output: nil, aliases: [], blocking: false, meta: {})
8
+ { path:, method:, input:, output:, aliases:, blocking:, meta: }
9
+ end
10
+
11
+ def self.token
12
+ output = io({ complete: { type: 'Boolean' }, token: { type: 'String' }, next_action: { type: 'String' } }, namespace: 'token')
13
+ {
14
+ http_header: 'X-Token', query_parameter: 'token',
15
+ resources: {
16
+ token: {
17
+ resources: {},
18
+ actions: {
19
+ request: action(path: '/mounted/v7/tokens', input: io({ user: { type: 'String' }, password: { type: 'String' },
20
+ lifetime: { type: 'String' }, interval: { type: 'Integer' } }), output:),
21
+ challenge: action(path: '/mounted/v7/tokens/challenge', input: io({ token: { type: 'String' }, code: { type: 'String' } }), output:),
22
+ revoke: action(path: '/mounted/v7/tokens/revoke')
23
+ }
24
+ }
25
+ }
26
+ }
27
+ end
28
+
29
+ def self.description
30
+ state = {
31
+ resources: {},
32
+ actions: {
33
+ show: action(path: '/mounted/v7/action_states/{action_state_id}', method: 'GET',
34
+ output: io({ id: { type: 'Integer' } }, namespace: 'action_state')),
35
+ poll: action(path: '/mounted/v7/action_states/{action_state_id}/poll', method: 'GET',
36
+ input: io({ timeout: { type: 'Float' }, update_in: { type: 'Float' },
37
+ status: { type: 'String' }, current: { type: 'Integer' }, total: { type: 'Integer' } }),
38
+ output: io({ finished: { type: 'Boolean' }, status: { type: 'String' },
39
+ current: { type: 'Integer' }, total: { type: 'Integer' } }, namespace: 'action_state')),
40
+ cancel: action(path: '/mounted/v7/action_states/{action_state_id}/cancel', method: 'POST')
41
+ }
42
+ }
43
+ run = action(
44
+ path: '/mounted/v7/clients/{client_id}/children/{child_id}', aliases: ['perform'],
45
+ input: io({ name: { type: 'String', nullable: true }, enabled: { type: 'Boolean' },
46
+ count: { type: 'Integer' }, unused: { type: 'String' } }),
47
+ output: io({ id: { type: 'Integer' }, name: { type: 'String', nullable: true } }),
48
+ meta: { global: { input: io({ includes: { type: 'Custom' } }, namespace: false) } }
49
+ )
50
+ {
51
+ authentication: { basic: {}, token: token, oauth2: { http_header: 'X-OAuth', revoke_url: '/revoke' } },
52
+ meta: { namespace: '_meta' },
53
+ resources: {
54
+ action_state: state,
55
+ owner: { resources: {}, actions: {
56
+ show: action(path: '/mounted/v7/owners/{owner_id}', method: 'GET',
57
+ output: io({ id: { type: 'Integer' }, label: { type: 'String' } }, namespace: 'owner'))
58
+ } },
59
+ client: { resources: { child: { resources: {}, actions: { client: run } } }, actions: {} },
60
+ authentication: { resources: {}, actions: {} },
61
+ hidden: { resources: {}, actions: { secret: action } }
62
+ }
63
+ }
64
+ end
65
+
66
+ def self.sample
67
+ { path_params: [42, 'a/b'], request: { name: nil, enabled: false, count: 0 },
68
+ response: { id: 1 }, status: true }
69
+ end
70
+ end
@@ -20,7 +20,7 @@ class ClientTestServer
20
20
  def start
21
21
  return if @wait_thr
22
22
 
23
- env = { 'BUNDLE_GEMFILE' => @gemfile }
23
+ env = { 'BUNDLE_GEMFILE' => @gemfile, 'BUNDLE_LOCKFILE' => "#{@gemfile}.lock" }
24
24
  cmd = ['bundle', 'exec', 'ruby', @server_script, '--port', '0']
25
25
  @stdin, @stdout, @wait_thr = Open3.popen2e(env, *cmd, chdir: @cwd)
26
26