haveapi-client 0.27.3 → 0.28.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f19892484687b15ed8dc7c1316dd17d45bd087f156adf9cd6ad5a225dce8dedd
4
- data.tar.gz: 422dea297e175322052513f595328cdf685c11e04cfe1fbcf15a014caa7c99aa
3
+ metadata.gz: 2e8b97599e4a2afdab12412cb851dced954f8efc0961a69055947cefd69edc28
4
+ data.tar.gz: ee5f1df9fa2e1687cbccf240c6de1a38cf66a6765621a7c56dbf1cd72213ded7
5
5
  SHA512:
6
- metadata.gz: f1cd0cb7f2278a71879e9575dc5fc71064b1bdb61cf87a65b17f3ae47f6e7ba754d8019cffefd1afeed275fea333bab67df2b7746f8fdd0e09b4ff443cd370a0
7
- data.tar.gz: 24f159b459283e909da0b70568b6b10565d2f61d5016f11423ac0f612601ddd7df4480fcac8fdb48ebcf84a30100ff637bdadaa4518539fed9b98b3c45ce0bc4
6
+ metadata.gz: 7fdd0d0e2b1cab27295094fd8ec99300e62226685fb223fcec684f1126ab611be2db1b9e0367b807c888748c665761af5baedca3b03f98750130a02e616ae2a9
7
+ data.tar.gz: c80f685e74a80e3e4ba440d380ef4d60fd566606d35e8b8ed19478567045b1f3539586d5786bea0a2a47f179fbf6a05f62c91b73d65e744307c3d3e66547ecb8
@@ -72,7 +72,7 @@ module HaveAPI::CLI
72
72
  end
73
73
 
74
74
  if (sep = ARGV.index('--'))
75
- cmd_opt.parse!(ARGV[sep + 1..])
75
+ cmd_opt.parse!(ARGV[(sep + 1)..])
76
76
  end
77
77
 
78
78
  c.exec(args[2..] || [])
@@ -80,7 +80,7 @@ module HaveAPI::CLI
80
80
  exit
81
81
  end
82
82
 
83
- if args.count == 1
83
+ if args.one?
84
84
  describe_resource(resources)
85
85
  exit
86
86
  end
@@ -374,7 +374,7 @@ module HaveAPI::CLI
374
374
 
375
375
  return {} unless sep
376
376
 
377
- @action_opt.parse!(ARGV[sep + 1..])
377
+ @action_opt.parse!(ARGV[(sep + 1)..])
378
378
 
379
379
  options
380
380
  end
@@ -646,7 +646,11 @@ module HaveAPI::CLI
646
646
  end
647
647
 
648
648
  def write_config
649
- File.write(config_path, YAML.dump(@config))
649
+ File.open(config_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |f|
650
+ f.write(YAML.dump(@config))
651
+ end
652
+
653
+ File.chmod(0o600, config_path)
650
654
  end
651
655
 
652
656
  def read_config
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  module HaveAPI::Client
2
4
  class Action
3
5
  attr_reader :client, :api, :name
@@ -29,9 +31,9 @@ module HaveAPI::Client
29
31
  params_arg = params.to_api
30
32
  end
31
33
 
32
- ret = @api.call(self, params_arg)
34
+ @api.call(self, params_arg)
35
+ ensure
33
36
  reset
34
- ret
35
37
  end
36
38
 
37
39
  def auth?
@@ -237,9 +239,15 @@ module HaveAPI::Client
237
239
  @prepared_help ||= @spec[:help].dup
238
240
 
239
241
  args.each do |arg|
240
- @prepared_path.sub!(/\{[a-zA-Z0-9\-_]+\}/, arg.to_s)
241
- @prepared_help.sub!(/\{[a-zA-Z0-9\-_]+\}/, arg.to_s)
242
+ encoded = encode_path_arg(arg)
243
+
244
+ @prepared_path.sub!(/\{[a-zA-Z0-9\-_]+\}/, encoded)
245
+ @prepared_help.sub!(/\{[a-zA-Z0-9\-_]+\}/, encoded)
242
246
  end
243
247
  end
248
+
249
+ def encode_path_arg(arg)
250
+ CGI.escape(arg.to_s).gsub('+', '%20')
251
+ end
244
252
  end
245
253
  end
@@ -3,6 +3,8 @@ require 'haveapi/client/authentication/base'
3
3
  module HaveAPI::Client::Authentication
4
4
  class Token < Base
5
5
  register :token
6
+ HTTP_HEADER_NAME = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
7
+
6
8
  attr_reader :token, :valid_to
7
9
 
8
10
  def setup
@@ -33,7 +35,7 @@ module HaveAPI::Client::Authentication
33
35
  return {} unless @configured
34
36
 
35
37
  check_validity
36
- @via == :header ? { @desc[:http_header] => @token } : {}
38
+ @via == :header ? { http_header => @token } : {}
37
39
  end
38
40
 
39
41
  def save
@@ -131,5 +133,12 @@ module HaveAPI::Client::Authentication
131
133
  def auth_action_input(name)
132
134
  @desc[:resources][:token][:actions][name][:input][:parameters].except(:token)
133
135
  end
136
+
137
+ def http_header
138
+ header = @desc[:http_header]
139
+ return header if header.is_a?(String) && header.match?(HTTP_HEADER_NAME)
140
+
141
+ raise ArgumentError, "invalid token authentication HTTP header name: #{header.inspect}"
142
+ end
134
143
  end
135
144
  end
@@ -100,7 +100,7 @@ class HaveAPI::Client::Client
100
100
 
101
101
  setup_api
102
102
 
103
- if @resources.include?(symbol)
103
+ if @resource_methods.include?(symbol)
104
104
  method(symbol).call(*args)
105
105
 
106
106
  else
@@ -112,7 +112,7 @@ class HaveAPI::Client::Client
112
112
  return super if @setup
113
113
 
114
114
  setup_api
115
- @resources.include?(symbol)
115
+ @resource_methods.include?(symbol)
116
116
  end
117
117
 
118
118
  private
@@ -120,17 +120,24 @@ class HaveAPI::Client::Client
120
120
  # Get the description from the API and setup resource methods.
121
121
  def setup_api
122
122
  @description = @api.describe_api(@version)
123
+ old_resource_methods = @resource_methods || {}
123
124
  @resources = {}
125
+ @resource_methods = {}
124
126
 
125
127
  @description[:resources].each do |name, desc|
126
128
  r = HaveAPI::Client::Resource.new(self, @api, name)
127
129
  r.setup(desc)
130
+ method_name = name.to_sym
128
131
 
129
- define_singleton_method(name) do |*args|
130
- tmp = r.dup
131
- tmp.prepared_args = args
132
- tmp.setup_from_clone(r)
133
- tmp
132
+ if old_resource_methods.include?(method_name) || define_resource_method?(name)
133
+ define_singleton_method(name) do |*args|
134
+ tmp = r.dup
135
+ tmp.prepared_args = args
136
+ tmp.setup_from_clone(r)
137
+ tmp
138
+ end
139
+
140
+ @resource_methods[method_name] = true
134
141
  end
135
142
 
136
143
  @resources[name] = r
@@ -138,4 +145,13 @@ class HaveAPI::Client::Client
138
145
 
139
146
  @setup = true
140
147
  end
148
+
149
+ def define_resource_method?(name)
150
+ method_name = name.to_sym
151
+
152
+ !singleton_class.public_method_defined?(method_name) &&
153
+ !singleton_class.protected_method_defined?(method_name) &&
154
+ !singleton_class.private_method_defined?(method_name, false) &&
155
+ !self.class.private_method_defined?(method_name, false)
156
+ end
141
157
  end
@@ -78,24 +78,26 @@ module HaveAPI::Client
78
78
  next unless define_method?(action, name)
79
79
 
80
80
  define_singleton_method(name) do |*args, **kwargs, &block|
81
+ call_action = action.dup
82
+ call_action.reset
81
83
  client_opts = @client.opts(:block, :block_interval, :block_timeout)
82
84
  all_args = @prepared_args + args
83
85
 
84
- if action.unresolved_args?
86
+ if call_action.unresolved_args?
85
87
  all_args.delete_if do |arg|
86
- break unless action.unresolved_args?
88
+ break unless call_action.unresolved_args?
87
89
 
88
- action.provide_args(arg)
90
+ call_action.provide_args(arg)
89
91
  true
90
92
  end
91
93
 
92
- if action.unresolved_args?
94
+ if call_action.unresolved_args?
93
95
  raise ArgumentError, 'one or more object ids missing'
94
96
  end
95
97
  end
96
98
 
97
99
  if all_args.length > 1 || (kwargs.any? && all_args.any?)
98
- raise ArgumentError, "too many arguments for action #{@name}##{action.name}"
100
+ raise ArgumentError, "too many arguments for action #{@name}##{call_action.name}"
99
101
  end
100
102
 
101
103
  arg = all_args.shift
@@ -111,7 +113,7 @@ module HaveAPI::Client
111
113
  end
112
114
 
113
115
  if user_params.nil?
114
- input_params = default_action_input_params(action)
116
+ input_params = default_action_input_params(call_action)
115
117
 
116
118
  else
117
119
  if user_params.has_key?(:meta)
@@ -122,25 +124,25 @@ module HaveAPI::Client
122
124
  end
123
125
  end
124
126
 
125
- input_params = default_action_input_params(action).update(user_params)
127
+ input_params = default_action_input_params(call_action).update(user_params)
126
128
  end
127
129
 
128
- ret = Response.new(action, action.execute(input_params))
130
+ ret = Response.new(call_action, call_action.execute(input_params))
129
131
 
130
132
  raise ActionFailed, ret unless ret.ok?
131
133
 
132
- return_value = case action.output && action.output_layout
134
+ return_value = case call_action.output && call_action.output_layout
133
135
  when :object
134
- ResourceInstance.new(@client, @api, self, action: action, response: ret)
136
+ ResourceInstance.new(@client, @api, self, action: call_action, response: ret)
135
137
 
136
138
  when :object_list
137
- ResourceInstanceList.new(@client, @api, self, action, ret)
139
+ ResourceInstanceList.new(@client, @api, self, call_action, ret)
138
140
 
139
141
  else # :hash, :hash_list
140
142
  ret
141
143
  end
142
144
 
143
- if action.blocking? && client_opts[:block]
145
+ if call_action.blocking? && client_opts[:block]
144
146
  wait_opts = {}
145
147
 
146
148
  {
@@ -156,6 +158,8 @@ module HaveAPI::Client
156
158
  end
157
159
 
158
160
  return_value
161
+ ensure
162
+ call_action.reset if call_action
159
163
  end
160
164
  end
161
165
  end
@@ -163,7 +167,12 @@ module HaveAPI::Client
163
167
  # Called before defining a method named +name+ that will
164
168
  # invoke +action+.
165
169
  def define_method?(action, name)
166
- return false if %i[new].include?(name.to_sym)
170
+ method_name = name.to_sym
171
+
172
+ return false if singleton_class.public_method_defined?(method_name)
173
+ return false if singleton_class.protected_method_defined?(method_name)
174
+ return false if singleton_class.private_method_defined?(method_name, false)
175
+ return false if self.class.private_method_defined?(method_name, false)
167
176
 
168
177
  true
169
178
  end
@@ -176,6 +185,8 @@ module HaveAPI::Client
176
185
  end
177
186
 
178
187
  def define_resource(resource)
188
+ return unless define_resource_method?(resource._name)
189
+
179
190
  define_singleton_method(resource._name) do |*args|
180
191
  tmp = resource.dup
181
192
  tmp.prepared_args = @prepared_args + args
@@ -183,5 +194,14 @@ module HaveAPI::Client
183
194
  tmp
184
195
  end
185
196
  end
197
+
198
+ def define_resource_method?(name)
199
+ method_name = name.to_sym
200
+
201
+ !singleton_class.public_method_defined?(method_name) &&
202
+ !singleton_class.protected_method_defined?(method_name) &&
203
+ !singleton_class.private_method_defined?(method_name, false) &&
204
+ !self.class.private_method_defined?(method_name, false)
205
+ end
186
206
  end
187
207
  end
@@ -1,6 +1,6 @@
1
1
  module HaveAPI
2
2
  module Client
3
3
  PROTOCOL_VERSION = '2.0'.freeze
4
- VERSION = '0.27.3'.freeze
4
+ VERSION = '0.28.0'.freeze
5
5
  end
6
6
  end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'uri'
5
+
6
+ RSpec.describe HaveAPI::Client::Action do
7
+ let(:action_spec) do
8
+ {
9
+ path: '/v1/users/{user_id}',
10
+ help: '/v1/users/{user_id}?describe=action',
11
+ method: 'GET',
12
+ auth: false,
13
+ blocking: false,
14
+ aliases: [],
15
+ input: {
16
+ layout: :hash,
17
+ namespace: :user,
18
+ parameters: {}
19
+ },
20
+ output: {
21
+ layout: :hash,
22
+ namespace: :user,
23
+ parameters: {}
24
+ },
25
+ meta: {}
26
+ }
27
+ end
28
+
29
+ let(:path_arg) { '42?user[name]=alice&_meta[includes]=group__secret' }
30
+ let(:encoded_arg) do
31
+ '42%3Fuser%5Bname%5D%3Dalice%26_meta%5Bincludes%5D%3Dgroup__secret'
32
+ end
33
+
34
+ let(:communicator) do
35
+ Class.new do
36
+ attr_reader :called_paths
37
+
38
+ def initialize
39
+ @called_paths = []
40
+ end
41
+
42
+ def url
43
+ 'https://api.example'
44
+ end
45
+
46
+ def describe_api(_version)
47
+ {
48
+ resources: {
49
+ users: {
50
+ actions: {
51
+ show: {
52
+ auth: false,
53
+ description: 'Show a user',
54
+ aliases: [],
55
+ blocking: false,
56
+ input: {
57
+ layout: 'hash',
58
+ namespace: 'user',
59
+ parameters: {
60
+ note: {
61
+ type: 'String',
62
+ nullable: false,
63
+ validators: {
64
+ present: {
65
+ empty: false,
66
+ message: 'required'
67
+ }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ output: {
73
+ layout: 'hash',
74
+ namespace: 'user',
75
+ parameters: {}
76
+ },
77
+ meta: {
78
+ object: nil,
79
+ global: nil
80
+ },
81
+ path: '/v1/users/{user_id}',
82
+ help: '/v1/users/{user_id}?method=GET',
83
+ method: 'GET'
84
+ }
85
+ },
86
+ resources: {}
87
+ }
88
+ }
89
+ }
90
+ end
91
+
92
+ def call(action, params = {})
93
+ @called_paths << action.prepared_path
94
+
95
+ {
96
+ status: true,
97
+ response: {
98
+ user: {
99
+ path: action.prepared_path,
100
+ params: params
101
+ }
102
+ }
103
+ }
104
+ end
105
+ end.new
106
+ end
107
+
108
+ let(:client) do
109
+ HaveAPI::Client::Client.new(
110
+ 'https://api.example',
111
+ communicator: communicator
112
+ ).tap(&:setup)
113
+ end
114
+
115
+ it 'encodes constructor path arguments as path components' do
116
+ action = described_class.new(nil, nil, :show, action_spec, [path_arg])
117
+ parsed = URI.parse("https://api.example#{action.prepared_path}")
118
+
119
+ expect(action.prepared_path).to eq("/v1/users/#{encoded_arg}")
120
+ expect(parsed.path).to eq("/v1/users/#{encoded_arg}")
121
+ expect(parsed.query).to be_nil
122
+ end
123
+
124
+ it 'encodes provided path arguments as path components' do
125
+ action = described_class.new(nil, nil, :show, action_spec, [])
126
+
127
+ action.provide_args(path_arg)
128
+
129
+ expect(action.prepared_path).to eq("/v1/users/#{encoded_arg}")
130
+ expect(action.prepared_help).to eq("/v1/users/#{encoded_arg}?describe=action")
131
+ end
132
+
133
+ it 'does not reuse path arguments after validation fails' do
134
+ expect do
135
+ client.users.show(42, {})
136
+ end.to raise_error(HaveAPI::Client::ValidationError)
137
+
138
+ expect do
139
+ client.users.show(note: 'allowed follow-up input')
140
+ end.to raise_error(ArgumentError, 'one or more object ids missing')
141
+
142
+ response = client.users.show(7, note: 'allowed follow-up input')
143
+
144
+ expect(communicator.called_paths).to eq(['/v1/users/7'])
145
+ expect(response[:path]).to eq('/v1/users/7')
146
+ end
147
+
148
+ it 'does not reuse path arguments after argument parsing fails' do
149
+ expect do
150
+ client.users.show(42, { note: 'valid input' }, { note: 'extra input' })
151
+ end.to raise_error(ArgumentError, 'too many arguments for action users#show')
152
+
153
+ expect do
154
+ client.users.show(note: 'allowed follow-up input')
155
+ end.to raise_error(ArgumentError, 'one or more object ids missing')
156
+
157
+ expect(communicator.called_paths).to eq([])
158
+ end
159
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe HaveAPI::Client::Authentication::Token do
6
+ let(:communicator) do
7
+ Class.new do
8
+ def url
9
+ 'https://api.example'
10
+ end
11
+
12
+ def verify_ssl
13
+ true
14
+ end
15
+ end.new
16
+ end
17
+
18
+ let(:http_header) { 'X-HaveAPI-Auth-Token' }
19
+
20
+ let(:description) do
21
+ {
22
+ http_header: http_header,
23
+ query_parameter: 'auth_token',
24
+ resources: { token: { actions: {} } }
25
+ }
26
+ end
27
+
28
+ context 'when sending tokens in HTTP headers' do
29
+ it 'uses RFC-safe token header names from the API description' do
30
+ http_header = 'X-HaveAPI-Auth-Token'
31
+ auth = described_class.new(
32
+ communicator,
33
+ description.merge(http_header: http_header),
34
+ token: 'secret-token'
35
+ )
36
+
37
+ expect(auth.request_headers).to eq(http_header => 'secret-token')
38
+ end
39
+
40
+ it 'rejects description-controlled header names that can inject headers' do
41
+ http_header = "X-HaveAPI-Auth-Token\r\nX-Injected-Token"
42
+ auth = described_class.new(
43
+ communicator,
44
+ description.merge(http_header: http_header),
45
+ token: 'secret-token'
46
+ )
47
+
48
+ expect { auth.request_headers }.to raise_error(
49
+ ArgumentError,
50
+ /invalid token authentication HTTP header name/
51
+ )
52
+ end
53
+
54
+ it 'rejects header names outside the HTTP token grammar' do
55
+ invalid_headers = [
56
+ nil,
57
+ '',
58
+ 'X HaveAPI Token',
59
+ 'X-HaveAPI-Token:',
60
+ "X-HaveAPI-Token\n"
61
+ ]
62
+
63
+ invalid_headers.each do |http_header|
64
+ auth = described_class.new(
65
+ communicator,
66
+ description.merge(http_header: http_header),
67
+ token: 'secret-token'
68
+ )
69
+
70
+ expect { auth.request_headers }.to raise_error(ArgumentError)
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'when sending tokens in query parameters' do
76
+ let(:http_header) { "X-HaveAPI-Auth-Token\r\nX-Injected-Token" }
77
+
78
+ it 'does not use the HTTP header name from the API description' do
79
+ auth = described_class.new(
80
+ communicator,
81
+ description,
82
+ token: 'secret-token',
83
+ via: :query_param
84
+ )
85
+
86
+ expect(auth.request_headers).to eq({})
87
+ expect(auth.request_query_params).to eq('auth_token' => 'secret-token')
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'haveapi/cli'
7
+
8
+ RSpec.describe HaveAPI::CLI::Cli do
9
+ around do |example|
10
+ old_umask = File.umask(0o022)
11
+
12
+ example.run
13
+ ensure
14
+ File.umask(old_umask) if old_umask
15
+ end
16
+
17
+ before do
18
+ allow(Dir).to receive(:home).and_return(home)
19
+ end
20
+
21
+ after do
22
+ FileUtils.rm_rf(home)
23
+ end
24
+
25
+ let(:home) { Dir.mktmpdir('haveapi-home-') }
26
+ let(:config_path) { File.join(home, '.haveapi-client.yml') }
27
+ let(:secret_token) { 'vuln75-secret-token' }
28
+ let(:config) do
29
+ {
30
+ servers: [
31
+ {
32
+ url: 'https://api.example',
33
+ auth: {
34
+ token: {
35
+ token: secret_token,
36
+ valid_to: 1_800_000_000
37
+ }
38
+ },
39
+ last_auth: :token
40
+ }
41
+ ]
42
+ }
43
+ end
44
+
45
+ def cli_with_config(config)
46
+ described_class.allocate.tap do |cli|
47
+ cli.instance_variable_set(:@config, config)
48
+ end
49
+ end
50
+
51
+ it 'creates saved credential config files with owner-only permissions' do
52
+ cli_with_config(config).send(:write_config)
53
+
54
+ expect(File.read(config_path)).to include(secret_token)
55
+ expect(File.stat(config_path).mode & 0o777).to eq(0o600)
56
+ end
57
+
58
+ it 'narrows existing saved credential config files before rewriting them' do
59
+ File.write(config_path, 'previous config')
60
+ File.chmod(0o644, config_path)
61
+
62
+ cli_with_config(config).send(:write_config)
63
+
64
+ expect(File.read(config_path)).to include(secret_token)
65
+ expect(File.stat(config_path).mode & 0o777).to eq(0o600)
66
+ end
67
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe HaveAPI::Client::Client do
6
+ let(:communicator_class) do
7
+ Struct.new(:description, :authenticate_calls, :describe_calls) do
8
+ def initialize(description)
9
+ super(description, 0, 0)
10
+ end
11
+
12
+ def describe_api(_version)
13
+ self.describe_calls += 1
14
+ description
15
+ end
16
+
17
+ def authenticate(*)
18
+ self.authenticate_calls += 1
19
+ :authenticated
20
+ end
21
+
22
+ def call(action, _params = {})
23
+ {
24
+ status: true,
25
+ response: {
26
+ action: action.name
27
+ }
28
+ }
29
+ end
30
+
31
+ def url
32
+ 'https://api.example'
33
+ end
34
+ end
35
+ end
36
+
37
+ def action_description(aliases: [])
38
+ {
39
+ auth: false,
40
+ description: 'test action',
41
+ aliases: aliases,
42
+ blocking: false,
43
+ input: {
44
+ layout: 'hash',
45
+ namespace: 'input',
46
+ parameters: {}
47
+ },
48
+ output: {
49
+ layout: 'hash',
50
+ namespace: 'output',
51
+ parameters: {}
52
+ },
53
+ meta: {
54
+ object: nil,
55
+ global: nil
56
+ },
57
+ path: '/v1/test',
58
+ help: '/v1/test?method=GET',
59
+ method: 'GET'
60
+ }
61
+ end
62
+
63
+ def client_for(resources)
64
+ api = communicator_class.new(resources: resources)
65
+ client = described_class.new(
66
+ 'https://api.example',
67
+ communicator: api
68
+ )
69
+
70
+ [client, api]
71
+ end
72
+
73
+ it 'does not let top-level resources replace existing client methods' do
74
+ client, api = client_for(
75
+ authenticate: {
76
+ actions: {},
77
+ resources: {}
78
+ },
79
+ setup: {
80
+ actions: {},
81
+ resources: {}
82
+ },
83
+ users: {
84
+ actions: {},
85
+ resources: {}
86
+ }
87
+ )
88
+
89
+ expect(client.method(:setup).owner).to eq(described_class)
90
+ expect(client.authenticate(:token, token: 'secret-token')).to eq(:authenticated)
91
+
92
+ client.setup
93
+
94
+ expect(client.method(:setup).owner).to eq(described_class)
95
+ expect(client.authenticate(:token, token: 'secret-token')).to eq(:authenticated)
96
+ expect(api.authenticate_calls).to eq(2)
97
+ expect(client.users).to be_a(HaveAPI::Client::Resource)
98
+ expect(client.resources.keys).to contain_exactly(:authenticate, :setup, :users)
99
+ end
100
+
101
+ it 'does not let actions or nested resources replace resource methods' do
102
+ client, = client_for(
103
+ users: {
104
+ actions: {
105
+ inspect: action_description,
106
+ show: action_description(aliases: %w[resources details])
107
+ },
108
+ resources: {
109
+ posts: {
110
+ actions: {},
111
+ resources: {}
112
+ },
113
+ setup: {
114
+ actions: {},
115
+ resources: {}
116
+ }
117
+ }
118
+ }
119
+ )
120
+
121
+ client.setup
122
+ users = client.users
123
+
124
+ expect(users.method(:inspect).owner).to eq(HaveAPI::Client::Resource)
125
+ expect(users.resources).to be_a(Hash)
126
+ expect(users.method(:setup).owner).to eq(HaveAPI::Client::Resource)
127
+ expect(users.show).to be_a(HaveAPI::Client::Response)
128
+ expect(users.details).to be_a(HaveAPI::Client::Response)
129
+ expect(users.posts).to be_a(HaveAPI::Client::Resource)
130
+ expect(users.actions.keys).to contain_exactly(:inspect, :show)
131
+ expect(users.resources.keys).to contain_exactly(:posts, :setup)
132
+ end
133
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.3
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -150,7 +150,10 @@ files:
150
150
  - lib/haveapi/client/validators/presence.rb
151
151
  - lib/haveapi/client/version.rb
152
152
  - lib/restclient_ext/resource.rb
153
- - shell.nix
153
+ - spec/action_security_spec.rb
154
+ - spec/authentication_token_security_spec.rb
155
+ - spec/cli_security_spec.rb
156
+ - spec/description_method_security_spec.rb
154
157
  - spec/integration/client_spec.rb
155
158
  - spec/integration/typed_input_spec.rb
156
159
  - spec/spec_helper.rb
data/shell.nix DELETED
@@ -1,20 +0,0 @@
1
- let
2
- pkgs = import <nixpkgs> {};
3
- stdenv = pkgs.stdenv;
4
-
5
- in stdenv.mkDerivation rec {
6
- name = "haveapi-client";
7
-
8
- buildInputs = with pkgs;[
9
- ruby
10
- git
11
- openssl
12
- ];
13
-
14
- shellHook = ''
15
- export GEM_HOME=$(pwd)/../../.gems
16
- export PATH="$GEM_HOME/bin:$PATH"
17
- gem install bundler
18
- bundle install
19
- '';
20
- }