haveapi 0.28.3 → 0.28.4

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,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'stringio'
5
+
6
+ module ExceptionMailerSpec
7
+ class RequestError < StandardError; end
8
+ class ActionError < StandardError; end
9
+ end
10
+
11
+ describe HaveAPI::Extensions::ExceptionMailer do
12
+ api do
13
+ define_resource(:Test) do
14
+ version 1
15
+ auth false
16
+
17
+ define_action(:RequestError) do
18
+ route 'request_error'
19
+ http_method :get
20
+
21
+ authorize do
22
+ raise ExceptionMailerSpec::RequestError, 'request boom'
23
+ end
24
+
25
+ def exec
26
+ ok!
27
+ end
28
+ end
29
+
30
+ define_action(:InputRequestError) do
31
+ route 'input_request_error'
32
+ http_method :post
33
+
34
+ authorize do
35
+ raise ExceptionMailerSpec::RequestError, 'request boom'
36
+ end
37
+
38
+ def exec
39
+ ok!
40
+ end
41
+ end
42
+
43
+ define_action(:ActionError) do
44
+ route 'action_error'
45
+ http_method :get
46
+ authorize { allow }
47
+
48
+ def exec
49
+ raise ExceptionMailerSpec::ActionError, 'action boom'
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ default_version 1
56
+
57
+ def with_exception_mailer(mailer)
58
+ action_hooks = HaveAPI::Hooks.hooks[HaveAPI::Action][:exec_exception]
59
+ original_action_listeners = action_hooks[:listeners].dup
60
+ server = app.settings.api_server
61
+ original_server_hooks = dup_instance_hooks(server)
62
+
63
+ mailer.enabled(server)
64
+ yield
65
+ ensure
66
+ action_hooks[:listeners] = original_action_listeners
67
+ restore_instance_hooks(server, original_server_hooks)
68
+ end
69
+
70
+ def dup_instance_hooks(server)
71
+ hooks = server.instance_variable_get(HaveAPI::Hooks::INSTANCE_VARIABLE)
72
+ return unless hooks
73
+
74
+ hooks.transform_values do |hook|
75
+ hook.merge(listeners: hook[:listeners].dup)
76
+ end
77
+ end
78
+
79
+ def restore_instance_hooks(server, hooks)
80
+ if hooks
81
+ server.instance_variable_set(HaveAPI::Hooks::INSTANCE_VARIABLE, hooks)
82
+ elsif server.instance_variable_defined?(HaveAPI::Hooks::INSTANCE_VARIABLE)
83
+ server.remove_instance_variable(HaveAPI::Hooks::INSTANCE_VARIABLE)
84
+ end
85
+ end
86
+
87
+ def collect_mail_calls(mailer)
88
+ calls = []
89
+
90
+ allow(mailer).to receive(:mail) do |context, exception, body|
91
+ calls << [context, exception, body]
92
+ end
93
+
94
+ calls
95
+ end
96
+
97
+ def new_mailer
98
+ described_class.new(
99
+ from: 'from@example.test',
100
+ to: 'to@example.test',
101
+ subject: '[spec] %s',
102
+ smtp: false
103
+ )
104
+ end
105
+
106
+ it 'mails request-level exceptions outside action execution' do
107
+ mailer = new_mailer
108
+ calls = collect_mail_calls(mailer)
109
+
110
+ with_exception_mailer(mailer) do
111
+ get '/v1/tests/request_error'
112
+ end
113
+
114
+ expect(last_response.status).to eq(500)
115
+ expect(api_response).not_to be_ok
116
+ expect(api_response.message).to eq('Server error occurred')
117
+
118
+ expect(calls.size).to eq(1)
119
+ context, exception, body = calls.first
120
+ expect(context.action.to_s).to include('RequestError')
121
+ expect(exception).to be_a(ExceptionMailerSpec::RequestError)
122
+ expect(body).to include('request boom')
123
+ end
124
+
125
+ it 'redacts secrets from request-level exception emails' do
126
+ mailer = new_mailer
127
+ calls = collect_mail_calls(mailer)
128
+ header_secret = %w[HEADER SECRET].join('_')
129
+ header_token_secret = %w[HEADER TOKEN SECRET].join('_')
130
+ cookie_secret = %w[COOKIE SECRET].join('_')
131
+ query_secret = %w[QUERY SECRET].join('_')
132
+ body_secret = %w[BODY SECRET].join('_')
133
+ nested_secret = %w[NESTED SECRET].join('_')
134
+
135
+ with_exception_mailer(mailer) do
136
+ header 'Accept', 'application/json'
137
+ header 'Content-Type', 'application/json'
138
+ header 'Authorization', "Bearer #{header_secret}"
139
+ header 'Cookie', "session=#{cookie_secret}"
140
+ header 'X-Auth-Token', header_token_secret
141
+ post "/v1/tests/input_request_error?_auth_token=#{query_secret}", JSON.generate({
142
+ password: body_secret,
143
+ nested: {
144
+ token: nested_secret,
145
+ visible: 'VISIBLE_VALUE'
146
+ }
147
+ })
148
+ end
149
+
150
+ body = calls.first[2]
151
+
152
+ expect(body).to include('VISIBLE_VALUE')
153
+ expect(body).to include(HaveAPI::Extensions::ExceptionMailer::FILTERED_VALUE)
154
+ [
155
+ header_secret,
156
+ header_token_secret,
157
+ cookie_secret,
158
+ query_secret,
159
+ body_secret,
160
+ nested_secret
161
+ ].each do |secret|
162
+ expect(body).not_to include(secret)
163
+ end
164
+ end
165
+
166
+ it 'mails action execution exceptions' do
167
+ mailer = new_mailer
168
+ calls = collect_mail_calls(mailer)
169
+
170
+ with_exception_mailer(mailer) do
171
+ get '/v1/tests/action_error'
172
+ end
173
+
174
+ expect(last_response.status).to eq(500)
175
+ expect(api_response).not_to be_ok
176
+ expect(calls.size).to eq(1)
177
+ expect(calls.first[1]).to be_a(ExceptionMailerSpec::ActionError)
178
+ expect(calls.first[2]).to include('action boom')
179
+ end
180
+
181
+ it 'does not replace the API response when mail delivery fails' do
182
+ mailer = new_mailer
183
+ allow(mailer).to receive(:mail).and_raise(StandardError, 'smtp down')
184
+
185
+ with_exception_mailer(mailer) do
186
+ expect do
187
+ get '/v1/tests/request_error'
188
+ end.to output(/ExceptionMailer failed: StandardError: smtp down/).to_stderr
189
+ end
190
+
191
+ expect(last_response.status).to eq(500)
192
+ expect(api_response).not_to be_ok
193
+ expect(api_response.message).to eq('Server error occurred')
194
+ end
195
+ end
@@ -37,6 +37,19 @@ describe HaveAPI::Server do
37
37
  { msg: params.dig(:test, :msg) }
38
38
  end
39
39
  end
40
+
41
+ define_action(:AuthorizeError) do
42
+ route 'authorize_error'
43
+ http_method :get
44
+
45
+ authorize do
46
+ raise 'authorize boom'
47
+ end
48
+
49
+ def exec
50
+ ok!
51
+ end
52
+ end
40
53
  end
41
54
 
42
55
  define_resource(:Transfer) do
@@ -143,6 +156,27 @@ describe HaveAPI::Server do
143
156
  expect(api_response).not_to be_ok
144
157
  end
145
158
 
159
+ it 'routes request-level exceptions through hook' do
160
+ calls = []
161
+
162
+ app.settings.api_server.connect_hook(:request_exception) do |ret, context, exception|
163
+ calls << [context, exception]
164
+ ret
165
+ end
166
+
167
+ header 'Accept', 'application/json'
168
+ get '/v1/tests/authorize_error'
169
+
170
+ expect(last_response.status).to eq(500)
171
+ expect(api_response).not_to be_ok
172
+ expect(api_response.message).to eq('Server error occurred')
173
+
174
+ expect(calls.size).to eq(1)
175
+ context, exception = calls.first
176
+ expect(context.action.to_s).to include('AuthorizeError')
177
+ expect(exception.message).to eq('authorize boom')
178
+ end
179
+
146
180
  it 'handles CORS preflight OPTIONS' do
147
181
  header 'Accept', 'application/json'
148
182
  header 'Origin', 'https://example.com'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.3
4
+ version: 0.28.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.28.3
32
+ version: 0.28.4
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.28.3
39
+ version: 0.28.4
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: json
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -190,12 +190,8 @@ files:
190
190
  - LICENSE.txt
191
191
  - README.md
192
192
  - Rakefile
193
- - doc/create-client.md
194
193
  - doc/hooks.erb
195
194
  - doc/index.md
196
- - doc/json-schema.html
197
- - doc/protocol.md
198
- - doc/protocol.png
199
195
  - haveapi.gemspec
200
196
  - lib/haveapi.rb
201
197
  - lib/haveapi/action.rb
@@ -312,6 +308,7 @@ files:
312
308
  - spec/documentation_spec.rb
313
309
  - spec/envelope_spec.rb
314
310
  - spec/extensions/action_exceptions_spec.rb
311
+ - spec/extensions/exception_mailer_spec.rb
315
312
  - spec/hooks_spec.rb
316
313
  - spec/model_adapters/active_record_spec.rb
317
314
  - spec/parameters/typed_spec.rb
data/doc/create-client.md DELETED
@@ -1,107 +0,0 @@
1
- # Client definition
2
- It is necessary to differentiate between clients for HaveAPI based APIs
3
- and clients to work with your API instance. This document describes
4
- the former. If you only want to use your API, you should check a list
5
- of available clients and pick the one in the right language. Only when
6
- there isn't a client already available in the language you want, then
7
- continue reading.
8
-
9
- # Design rules
10
- The client must completely depend on the API description:
11
-
12
- - it has no assumptions and API-specific code,
13
- - it does not know any resources, actions and parameters,
14
- - everything the client knows must be found out from the API description.
15
-
16
- All clients should use a similar paradigm, so that it is possible to use
17
- clients in different languages in the same way, as far as the language syntax
18
- allows. Clients bundled with HaveAPI may serve as a model. A client should
19
- use all the advantages and coding styles of the language it is written
20
- in (e.g. use objects in object oriented languages).
21
-
22
- A model paradigm (in no particular language):
23
-
24
- // Create client instance
25
- api = new HaveAPI.Client("https://your.api.tld")
26
-
27
- // Authenticate
28
- api.authenticate("basic", {"user": "yourname", "password": "yourpassword"})
29
-
30
- // Access resources and actions
31
- api.<resource>.<action>( { <parameters> } )
32
- api.user.new({"name": "Very Name", "password": "donottellanyone"})
33
- api.user.list()
34
- api.nested.resource.deep.list()
35
-
36
- // Pass IDs to resources
37
- api.nested(1).resource(2).deep.list()
38
-
39
- // Object-like access
40
- user = api.user.show(1)
41
- user.id
42
- user.name
43
- user.destroy()
44
-
45
- // Logout
46
- api.logout()
47
-
48
- # Necessary features to implement
49
- A client should implement all of the listed features in order to be useful.
50
-
51
- ## Resource tree
52
- The client gives access to all listed resources and their actions.
53
-
54
- In scripting languages, resources and actions are usually defined as dynamic
55
- properties/objects/methods, depending on the language.
56
-
57
- ## Input/output parameters
58
- Allow sending input parameters and accessing the response.
59
-
60
- ## Input/output formats
61
- A client must send appropriate HTTP header `Accept`. As only JSON is by now built-in
62
- in HaveAPI, it should send `application/json`.
63
-
64
- ## Authentication
65
- All authentication methods that are built-in the HaveAPI should be supported
66
- if possible. The client should choose suitable authentication method
67
- for its purpose and allow the developer to select the authentication method
68
- if it makes sense to do so.
69
-
70
- It is a good practise to implement authentication methods as plugins,
71
- so that developers may add custom authentication providers easily.
72
-
73
- ## Object-like access
74
- A client must interpret the API response according to action output layout.
75
- Layouts `object` and `object_list` should be handled as object instances,
76
- if the language allows it.
77
-
78
- Object instances represent the object fetched from the database. Received
79
- parameters are accessed via object attributes/properties. Actions are defined
80
- as instance methods. Objects may have associations to other resources which
81
- must be made available and be easy to access.
82
-
83
- # Supplemental features
84
- Following features are supplemental. It is good to support them,
85
- but is not necessary.
86
-
87
- ## Client-side validations
88
- Client may make use of described validators and validate the input before
89
- sending it to the API, to lessen the API load and make it more user-friendly.
90
-
91
- However, as the input is validated on the server anyway, it does not have
92
- to be implemented.
93
-
94
- ## Metadata channel
95
- Metadata channel is currently used to specify what associated resources should
96
- be prefetched and whether an object list should contain total number of items.
97
-
98
- Metadata is nothing more than a hash in input parameters under key `_meta`.
99
-
100
- ## Blocking actions
101
- Useful for APIs with long-running actions. Clients can check state of such actions
102
- using resource `ActionState`. Because this resource is automatically present in all
103
- APIs that use blocking actions, client libraries expose this resource to the developer
104
- just as any other resource in the API.
105
-
106
- However, you may wish to integrate it in your client and provide ways for the action
107
- call to block/accept callbacks.