heroics 0.0.1

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,19 @@
1
+ module Heroics
2
+ # Process a name to make it suitable for use as a Ruby method name.
3
+ #
4
+ # @param name [String] The name to process.
5
+ # @return [String] The new name with capitals converted to lowercase, and
6
+ # dashes and spaces converted to underscores.
7
+ def self.ruby_name(name)
8
+ name.downcase.gsub(/[- ]/, '_')
9
+ end
10
+
11
+ # Process a name to make it suitable for use as a pretty command name.
12
+ #
13
+ # @param name [String] The name to process.
14
+ # @return [String] The new name with capitals converted to lowercase, and
15
+ # underscores and spaces converted to dashes.
16
+ def self.pretty_name(name)
17
+ name.downcase.gsub(/[_ ]/, '-')
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module Heroics
2
+ # A resource with methods mapped to API links.
3
+ class Resource
4
+ # Instantiate a resource.
5
+ #
6
+ # @param links [Hash<String,Link>] A hash that maps method names to links.
7
+ def initialize(links)
8
+ @links = links
9
+ end
10
+
11
+ private
12
+
13
+ # Find a link and invoke it.
14
+ #
15
+ # @param name [String] The name of the method to invoke.
16
+ # @param parameters [Array] The arguments to pass to the method. This
17
+ # should always be a `Hash` mapping parameter names to values.
18
+ # @raise [NoMethodError] Raised if the name doesn't match a known link.
19
+ # @return [String,Array,Hash] The response received from the server. JSON
20
+ # responses are automatically decoded into Ruby objects.
21
+ def method_missing(name, *parameters)
22
+ link = @links[name.to_s]
23
+ if link.nil?
24
+ address = "<#{self.class.name}:0x00#{(self.object_id << 1).to_s(16)}>"
25
+ raise NoMethodError.new("undefined method `#{name}' for ##{address}")
26
+ end
27
+ link.run(*parameters)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,242 @@
1
+ module Heroics
2
+ # A wrapper around a bare JSON schema to make it easier to use.
3
+ class Schema
4
+ attr_reader :schema
5
+
6
+ # Instantiate a schema.
7
+ #
8
+ # @param schema [Hash] The bare JSON schema to wrap.
9
+ def initialize(schema)
10
+ @schema = schema
11
+ @resources = {}
12
+ @schema['definitions'].each_key do |name|
13
+ @resources[name] = ResourceSchema.new(@schema, name)
14
+ end
15
+ end
16
+
17
+ # Get a schema for a named resource.
18
+ #
19
+ # @param name [String] The name of the resource.
20
+ # @raise [SchemaError] Raised if an unknown resource name is provided.
21
+ def resource(name)
22
+ resource_schema = @resources[name]
23
+ if @schema['definitions'].has_key?(name)
24
+ ResourceSchema.new(@schema, name)
25
+ else
26
+ raise SchemaError.new("Unknown resource '#{name}'.")
27
+ end
28
+ end
29
+
30
+ # The resource schema children that are part of this schema.
31
+ #
32
+ # @return [Array<ResourceSchema>] The resource schema children.
33
+ def resources
34
+ @resources.values
35
+ end
36
+ end
37
+
38
+ # A wrapper around a bare resource element in a JSON schema to make it
39
+ # easier to use.
40
+ class ResourceSchema
41
+ attr_reader :name
42
+
43
+ # Instantiate a resource schema.
44
+ #
45
+ # @param schema [Hash] The bare JSON schema to wrap.
46
+ # @param name [String] The name of the resource to identify in the schema.
47
+ def initialize(schema, name)
48
+ @schema = schema
49
+ @name = name
50
+ link_schema = schema['definitions'][name]['links']
51
+ @links = Hash[link_schema.each_with_index.map do |link, link_index|
52
+ link_name = Heroics.ruby_name(link['title'])
53
+ [link_name, LinkSchema.new(schema, name, link_index)]
54
+ end]
55
+ end
56
+
57
+ # Get a schema for a named link.
58
+ #
59
+ # @param name [String] The name of the link.
60
+ # @raise [SchemaError] Raised if an unknown link name is provided.
61
+ def link(name)
62
+ schema = @links[name]
63
+ raise SchemaError.new("Unknown link '#{name}'.") unless schema
64
+ schema
65
+ end
66
+
67
+ # The link schema children that are part of this resource schema.
68
+ #
69
+ # @return [Array<LinkSchema>] The link schema children.
70
+ def links
71
+ @links.values
72
+ end
73
+ end
74
+
75
+ # A wrapper around a bare link element for a resource in a JSON schema to
76
+ # make it easier to use.
77
+ class LinkSchema
78
+ attr_reader :name, :resource_name, :description
79
+
80
+ # Instantiate a link schema.
81
+ #
82
+ # @param schema [Hash] The bare JSON schema to wrap.
83
+ # @param resource_name [String] The name of the resource to identify in
84
+ # the schema.
85
+ # @param link_index [Fixnum] The index of the link in the resource schema.
86
+ def initialize(schema, resource_name, link_index)
87
+ @schema = schema
88
+ @resource_name = resource_name
89
+ @link_index = link_index
90
+ @name = Heroics.ruby_name(link_schema['title'])
91
+ @description = link_schema['description']
92
+ end
93
+
94
+ # Get the resource name in pretty form.
95
+ #
96
+ # @return [String] The pretty resource name.
97
+ def pretty_resource_name
98
+ Heroics.pretty_name(resource_name)
99
+ end
100
+
101
+ # Get the link name in pretty form.
102
+ #
103
+ # @return [String] The pretty link name.
104
+ def pretty_name
105
+ Heroics.pretty_name(name)
106
+ end
107
+
108
+ # Get the HTTP method for this link.
109
+ #
110
+ # @return [Symbol] The HTTP method.
111
+ def method
112
+ link_schema['method'].downcase.to_sym
113
+ end
114
+
115
+ # Get the names of the parameters this link expects.
116
+ #
117
+ # @return [Array<String>] The parameters.
118
+ def parameters
119
+ resolve_parameters(link_schema['href'].scan(PARAMETER_REGEX))
120
+ end
121
+
122
+ # Get an example request body.
123
+ #
124
+ # @return [Hash] A sample request body.
125
+ def example_body
126
+ if body_schema = link_schema['schema']
127
+ definitions = @schema['definitions'][@resource_name]['definitions']
128
+ Hash[body_schema['properties'].keys.map do |property|
129
+ # FIXME This is wrong! -jkakar
130
+ if definitions.has_key?(property)
131
+ example = definitions[property]['example']
132
+ else
133
+ example = ''
134
+ end
135
+ [property, example]
136
+ end]
137
+ end
138
+ end
139
+
140
+ # Inject parameters into the link href and return the body, if it exists.
141
+ #
142
+ # @param parameters [Array] The list of parameters to inject into the
143
+ # path.
144
+ # @raise [ArgumentError] Raised if either too many or too few parameters
145
+ # were provided.
146
+ # @return [String,Object] A path and request body pair. The body value is
147
+ # nil if a payload wasn't included in the list of parameters.
148
+ def format_path(parameters)
149
+ path = link_schema['href']
150
+ parameter_size = path.scan(PARAMETER_REGEX).size
151
+ too_few_parameters = parameter_size > parameters.size
152
+ # FIXME We should use the schema to detect when a request body is
153
+ # permitted and do the calculation correctly here. -jkakar
154
+ too_many_parameters = parameter_size < (parameters.size - 1)
155
+ if too_few_parameters || too_many_parameters
156
+ raise ArgumentError.new("wrong number of arguments " +
157
+ "(#{parameters.size} for #{parameter_size})")
158
+ end
159
+
160
+ (0..parameter_size).each do |i|
161
+ path = path.sub(PARAMETER_REGEX, format_parameter(parameters[i]))
162
+ end
163
+ body = parameters.slice(parameter_size)
164
+ return path, body
165
+ end
166
+
167
+ private
168
+
169
+ # Match parameters in definition strings.
170
+ PARAMETER_REGEX = /\{\([%\/a-zA-Z0-9_]*\)\}/
171
+
172
+ # Get the raw link schema.
173
+ #
174
+ # @param [Hash] The raw link schema.
175
+ def link_schema
176
+ @schema['definitions'][@resource_name]['links'][@link_index]
177
+ end
178
+
179
+ # Get the names of the parameters this link expects.
180
+ #
181
+ # @param parameters [Array] The names of the parameter definitions to
182
+ # convert to parameter names.
183
+ # @return [Array<String>] The parameters.
184
+ def resolve_parameters(parameters)
185
+ # FIXME This is all pretty terrible. It'd be much better to
186
+ # automatically resolve $ref's based on the path instead of special
187
+ # casing everything. -jkakar
188
+ properties = @schema['definitions'][@resource_name]['properties']
189
+ definitions = Hash[properties.each_pair.map do |key, value|
190
+ [value['$ref'], key]
191
+ end]
192
+ parameters.map do |parameter|
193
+ definition_name = URI.unescape(parameter[2..-3])
194
+ if definitions.has_key?(definition_name)
195
+ definitions[definition_name]
196
+ else
197
+ definition_name = definition_name.split('/')[-1]
198
+ resource_definitions = @schema[
199
+ 'definitions'][@resource_name]['definitions'][definition_name]
200
+ if resource_definitions.has_key?('anyOf')
201
+ resource_definitions['anyOf'].map do |property|
202
+ definitions[property['$ref']]
203
+ end.join('|')
204
+ else
205
+ resource_definitions['oneOf'].map do |property|
206
+ definitions[property['$ref']]
207
+ end.join('|')
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ # Convert a path parameter to a format suitable for use in a path.
214
+ #
215
+ # @param [Fixnum,String,TrueClass,FalseClass,Time] The parameter to format.
216
+ # @return [String] The formatted parameter.
217
+ def format_parameter(parameter)
218
+ parameter.instance_of?(Time) ? iso_format(parameter) : parameter.to_s
219
+ end
220
+
221
+ # Convert a time to an ISO 8601 combined data and time format.
222
+ #
223
+ # @param time [Time] The time to convert to ISO 8601 format.
224
+ # @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
225
+ def iso_format(time)
226
+ time.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
227
+ end
228
+ end
229
+
230
+ # Download a JSON schema from a URL.
231
+ #
232
+ # @param url [String] The URL for the schema.
233
+ # @param options [Hash] Configuration for links. Possible keys include:
234
+ # - default_headers: Optionally, a set of headers to include in every
235
+ # request made by the client. Default is no custom headers.
236
+ # @return [Schema] The downloaded JSON schema.
237
+ def self.download_schema(url, options={})
238
+ default_headers = options.fetch(:default_headers, {})
239
+ response = Excon.get(url, headers: default_headers, expects: [200, 201])
240
+ Schema.new(MultiJson.decode(response.body))
241
+ end
242
+ end
@@ -0,0 +1,3 @@
1
+ module Heroics
2
+ VERSION = "0.0.1"
3
+ end
data/test.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'netrc'
2
+
3
+ _, token = Netrc.read['api.heroku.com']
4
+
5
+ require './lib/heroics'
6
+
7
+ heroics = Heroics.new(
8
+ :cache => Heroics::FileCache.new(token),
9
+ :token => token
10
+ )
11
+
12
+ heroics.apps.list
13
+ apps = heroics.apps.list # should use cache
14
+ puts(apps)
15
+ puts
16
+
17
+ app = heroics.apps.info('stringer-geemus')
18
+ puts(app)
19
+ puts
20
+
21
+ puts(heroics.apps('stringer-geemus'))
22
+ puts
23
+
24
+ collaborators = app.collaborators.list
25
+ puts(collaborators)
26
+ puts
27
+
28
+ collaborator_id = collaborators.first.id
29
+ collaborator = app.collaborators.info(collaborator_id)
30
+ puts(collaborator)
31
+ puts
32
+
33
+ regions = heroics.regions.list
34
+ puts(regions)
35
+ puts
36
+
37
+ region_id = regions.first.id
38
+ puts(heroics.regions.info(region_id))
39
+ puts
40
+
41
+ puts(heroics.regions(region_id))
42
+ puts
data/test/cli_test.rb ADDED
@@ -0,0 +1,282 @@
1
+ require 'helper'
2
+ require 'stringio'
3
+
4
+ class CLITest < MiniTest::Unit::TestCase
5
+ include ExconHelper
6
+
7
+ # CLI.run displays usage information when no arguments are provided.
8
+ def test_run_without_arguments
9
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
10
+ client = Heroics::client_from_schema(schema, 'https://example.com')
11
+ output = StringIO.new
12
+ command1 = Heroics::Command.new(
13
+ 'cli', schema.resource('resource').link('list'), client, output)
14
+ command2 = Heroics::Command.new(
15
+ 'cli', schema.resource('resource').link('info'), client, output)
16
+ cli = Heroics::CLI.new('cli', {'resource:list' => command1,
17
+ 'resource:info' => command2}, output)
18
+ cli.run
19
+ expected = <<-USAGE
20
+ Usage: cli <command> [<parameter> [...]] [<body>]
21
+
22
+ Help topics, type "cli help <topic>" for more details:
23
+
24
+ resource:info Show a sample resource
25
+ resource:list Show all sample resources
26
+ USAGE
27
+ assert_equal(expected, output.string)
28
+ end
29
+
30
+ # CLI.run displays usage information when the help command is specified.
31
+ def test_run_with_help_command
32
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
33
+ client = Heroics::client_from_schema(schema, 'https://example.com')
34
+ output = StringIO.new
35
+ command1 = Heroics::Command.new(
36
+ 'cli', schema.resource('resource').link('list'), client, output)
37
+ command2 = Heroics::Command.new(
38
+ 'cli', schema.resource('resource').link('info'), client, output)
39
+ cli = Heroics::CLI.new('cli', {'resource:list' => command1,
40
+ 'resource:info' => command2}, output)
41
+ cli.run('help')
42
+ expected = <<-USAGE
43
+ Usage: cli <command> [<parameter> [...]] [<body>]
44
+
45
+ Help topics, type "cli help <topic>" for more details:
46
+
47
+ resource:info Show a sample resource
48
+ resource:list Show all sample resources
49
+ USAGE
50
+ assert_equal(expected, output.string)
51
+ end
52
+
53
+ # CLI.run displays command-specific help when a command name is included
54
+ # with the 'help' command.
55
+ def test_run_with_help_command_and_explicit_command_name
56
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
57
+ client = Heroics::client_from_schema(schema, 'https://example.com')
58
+ output = StringIO.new
59
+ command1 = Heroics::Command.new(
60
+ 'cli', schema.resource('resource').link('list'), client, output)
61
+ command2 = Heroics::Command.new(
62
+ 'cli', schema.resource('resource').link('info'), client, output)
63
+ cli = Heroics::CLI.new('cli', {'resource:list' => command1,
64
+ 'resource:info' => command2}, output)
65
+ cli.run('help', 'resource:info')
66
+ expected = <<-USAGE
67
+ Usage: cli resource:info <uuid_field>
68
+
69
+ Description:
70
+ Show a sample resource
71
+ USAGE
72
+ assert_equal(expected, output.string)
73
+ end
74
+
75
+ # CLI.run displays an error message when no commands have been registered.
76
+ def test_run_without_commands
77
+ output = StringIO.new
78
+ cli = Heroics::CLI.new('cli', {}, output)
79
+ cli.run('help')
80
+ assert_equal('No commands are available.', output.string)
81
+ end
82
+
83
+ # CLI.run displays an error message when an unknown command name is used.
84
+ def test_run_with_unknown_name
85
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
86
+ client = Heroics::client_from_schema(schema, 'https://example.com')
87
+ output = StringIO.new
88
+ command = Heroics::Command.new(
89
+ 'cli', schema.resource('resource').link('list'), client, output)
90
+ cli = Heroics::CLI.new('cli', {'resource:list' => command}, output)
91
+ cli.run('unknown:command')
92
+ assert_equal("There is no command called 'unknown:command'.\n",
93
+ output.string)
94
+ end
95
+
96
+ # CLI.run runs the command matching the specified name.
97
+ def test_run_with_dashed_command_name
98
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
99
+ client = Heroics::client_from_schema(schema, 'https://example.com')
100
+ output = StringIO.new
101
+ command = Heroics::Command.new(
102
+ 'cli', schema.resource('resource').link('identify_resource'), client,
103
+ output)
104
+ cli = Heroics::CLI.new('cli', {'resource:identify-resource' => command},
105
+ output)
106
+
107
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
108
+ Excon.stub(method: :get) do |request|
109
+ assert_equal("/resource/#{uuid}", request[:path])
110
+ Excon.stubs.pop
111
+ {status: 200}
112
+ end
113
+
114
+ cli.run('resource:identify-resource', uuid)
115
+ assert_equal('', output.string)
116
+ end
117
+
118
+ # CLI.run runs the resource matching the specified name.
119
+ def test_run_with_dashed_resource_name
120
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
121
+ client = Heroics::client_from_schema(schema, 'https://example.com')
122
+ output = StringIO.new
123
+ command = Heroics::Command.new(
124
+ 'cli', schema.resource('another-resource').link('list'), client, output)
125
+ cli = Heroics::CLI.new('cli', {'another-resource:list' => command},
126
+ output)
127
+
128
+ result = {'Hello' => 'World!'}
129
+ Excon.stub(method: :get) do |request|
130
+ assert_equal("/another-resource", request[:path])
131
+ Excon.stubs.pop
132
+ {status: 200, headers: {'Content-Type' => 'application/json'},
133
+ body: MultiJson.dump(result)}
134
+ end
135
+
136
+ cli.run('another-resource:list')
137
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
138
+ end
139
+
140
+ # CLI.run runs the command matching the specified name and passes parameters
141
+ # to it.
142
+ def test_run_with_parameters
143
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
144
+ client = Heroics::client_from_schema(schema, 'https://example.com')
145
+ output = StringIO.new
146
+ command = Heroics::Command.new(
147
+ 'cli', schema.resource('resource').link('update'), client, output)
148
+ cli = Heroics::CLI.new('cli', {'resource:update' => command}, output)
149
+
150
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
151
+ body = {'Hello' => 'World!'}
152
+ result = {'Goodbye' => 'Universe!'}
153
+ Excon.stub(method: :patch) do |request|
154
+ assert_equal("/resource/#{uuid}", request[:path])
155
+ assert_equal('application/json', request[:headers]['Content-Type'])
156
+ assert_equal(body, MultiJson.load(request[:body]))
157
+ Excon.stubs.pop
158
+ {status: 200, headers: {'Content-Type' => 'application/json'},
159
+ body: MultiJson.dump(result)}
160
+ end
161
+
162
+ cli.run('resource:update', uuid, body)
163
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
164
+ end
165
+ end
166
+
167
+ class CLIFromSchemaTest < MiniTest::Unit::TestCase
168
+ include ExconHelper
169
+
170
+ # cli_from_schema returns a CLI generated from the specified schema.
171
+ def test_cli_from_schema
172
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
173
+ body = {'Hello' => 'World!'}
174
+ result = {'Goodbye' => 'Universe!'}
175
+ Excon.stub(method: :patch) do |request|
176
+ assert_equal("/resource/#{uuid}", request[:path])
177
+ assert_equal('application/json', request[:headers]['Content-Type'])
178
+ assert_equal(body, MultiJson.load(request[:body]))
179
+ Excon.stubs.pop
180
+ {status: 200, headers: {'Content-Type' => 'application/json'},
181
+ body: MultiJson.dump(result)}
182
+ end
183
+
184
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
185
+ output = StringIO.new
186
+ cli = Heroics.cli_from_schema('cli', output, schema, 'https://example.com')
187
+ cli.run('resource:update', uuid, body)
188
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
189
+ end
190
+
191
+ # cli_from_schema optionally accepts custom headers to pass with every
192
+ # request made by the generated CLI.
193
+ def test_cli_from_schema_with_custom_headers
194
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
195
+ body = {'Hello' => 'World!'}
196
+ result = {'Goodbye' => 'Universe!'}
197
+ Excon.stub(method: :patch) do |request|
198
+ assert_equal('application/vnd.heroku+json; version=3',
199
+ request[:headers]['Accept'])
200
+ Excon.stubs.pop
201
+ {status: 200, headers: {'Content-Type' => 'application/json'},
202
+ body: MultiJson.dump(result)}
203
+ end
204
+
205
+ schema = Heroics::Schema.new(SAMPLE_SCHEMA)
206
+ output = StringIO.new
207
+ cli = Heroics.cli_from_schema(
208
+ 'cli', output, schema, 'https://example.com',
209
+ default_headers: {'Accept' => 'application/vnd.heroku+json; version=3'})
210
+ cli.run('resource:update', uuid, body)
211
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
212
+ end
213
+ end
214
+
215
+ class CLIFromSchemaURLTest < MiniTest::Unit::TestCase
216
+ include ExconHelper
217
+
218
+ # client_from_schema_url downloads a schema and returns a Client generated
219
+ # from it.
220
+ def test_cli_from_schema_url
221
+ Excon.stub(method: :get) do |request|
222
+ assert_equal('example.com', request[:host])
223
+ assert_equal('/schema', request[:path])
224
+ Excon.stubs.pop
225
+ {status: 200, headers: {'Content-Type' => 'application/json'},
226
+ body: MultiJson.dump(SAMPLE_SCHEMA)}
227
+ end
228
+
229
+ output = StringIO.new
230
+ cli = Heroics.cli_from_schema_url('cli', output,
231
+ 'https://example.com/schema')
232
+
233
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
234
+ body = {'Hello' => 'World!'}
235
+ result = {'Goodbye' => 'Universe!'}
236
+ Excon.stub(method: :patch) do |request|
237
+ assert_equal("/resource/#{uuid}", request[:path])
238
+ assert_equal('application/json', request[:headers]['Content-Type'])
239
+ assert_equal(body, MultiJson.load(request[:body]))
240
+ Excon.stubs.pop
241
+ {status: 200, headers: {'Content-Type' => 'application/json'},
242
+ body: MultiJson.dump(result)}
243
+ end
244
+
245
+ cli.run('resource:update', uuid, body)
246
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
247
+ end
248
+
249
+ # cli_from_schema_url optionally accepts custom headers to include in the
250
+ # request to download the schema. The same headers are passed in requests
251
+ # made by the generated CLI.
252
+ def test_cli_from_schema_url_with_custom_headers
253
+ Excon.stub(method: :get) do |request|
254
+ assert_equal('example.com', request[:host])
255
+ assert_equal('/schema', request[:path])
256
+ assert_equal('application/vnd.heroku+json; version=3',
257
+ request[:headers]['Accept'])
258
+ Excon.stubs.pop
259
+ {status: 200, headers: {'Content-Type' => 'application/json'},
260
+ body: MultiJson.dump(SAMPLE_SCHEMA)}
261
+ end
262
+
263
+ output = StringIO.new
264
+ cli = Heroics.cli_from_schema_url(
265
+ 'cli', output, 'https://example.com/schema',
266
+ default_headers: {'Accept' => 'application/vnd.heroku+json; version=3'})
267
+
268
+ uuid = '1ab1c589-df46-40aa-b786-60e83b1efb10'
269
+ body = {'Hello' => 'World!'}
270
+ result = {'Goodbye' => 'Universe!'}
271
+ Excon.stub(method: :patch) do |request|
272
+ assert_equal('application/vnd.heroku+json; version=3',
273
+ request[:headers]['Accept'])
274
+ Excon.stubs.pop
275
+ {status: 200, headers: {'Content-Type' => 'application/json'},
276
+ body: MultiJson.dump(result)}
277
+ end
278
+
279
+ cli.run('resource:update', uuid, body)
280
+ assert_equal(MultiJson.dump(result, pretty: true) + "\n", output.string)
281
+ end
282
+ end