hoodoo 1.0.2

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.
Files changed (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'raygun4ruby'
3
+
4
+ # This doesn't test the Airbrake gem / configuration itself - just check that
5
+ # the appropriate Airbrake method gets called.
6
+
7
+ describe Hoodoo::Services::Middleware::ExceptionReporting::AirbrakeReporter do
8
+
9
+ before :each do
10
+ Hoodoo::Services::Middleware::ExceptionReporting.add( described_class )
11
+ end
12
+
13
+ after :each do
14
+ Hoodoo::Services::Middleware::ExceptionReporting.wait()
15
+ Hoodoo::Services::Middleware::ExceptionReporting.remove( described_class )
16
+ end
17
+
18
+ it 'calls Airbrake' do
19
+ Hoodoo::Services::Middleware::ExceptionReporting.add( described_class )
20
+ ex = RuntimeError.new( 'A' )
21
+ expect( Airbrake ).to receive( :notify_or_ignore ).once.with( ex, { :rack_env => nil } )
22
+ Hoodoo::Services::Middleware::ExceptionReporting.report( ex )
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'airbrake'
3
+
4
+ # This doesn't test the Raygun gem / configuration itself - just check that
5
+ # the appropriate Raygun method gets called.
6
+
7
+ describe Hoodoo::Services::Middleware::ExceptionReporting::RaygunReporter do
8
+
9
+ before :each do
10
+ Hoodoo::Services::Middleware::ExceptionReporting.add( described_class )
11
+ end
12
+
13
+ after :each do
14
+ Hoodoo::Services::Middleware::ExceptionReporting.wait()
15
+ Hoodoo::Services::Middleware::ExceptionReporting.remove( described_class )
16
+ end
17
+
18
+ it 'calls Raygun' do
19
+ ex = RuntimeError.new( 'A' )
20
+ expect( Raygun ).to receive( :track_exception ).once.with( ex, nil )
21
+ Hoodoo::Services::Middleware::ExceptionReporting.report( ex )
22
+ end
23
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ class TestCORSImplementation < Hoodoo::Services::Implementation
4
+ def show( context )
5
+ context.response.body = { 'show' => 'the thing', 'the_thing' => context.request.ident }
6
+ end
7
+ end
8
+
9
+ class TestCORSInterface < Hoodoo::Services::Interface
10
+ interface :TestCORS do
11
+ endpoint :test_cors, TestCORSImplementation
12
+ actions :show
13
+ end
14
+ end
15
+
16
+ class TestCORSService < Hoodoo::Services::Service
17
+ comprised_of TestCORSInterface
18
+ end
19
+
20
+ describe Hoodoo::Services::Middleware do
21
+ def app
22
+ Rack::Builder.new do
23
+ use Hoodoo::Services::Middleware
24
+ run TestCORSService.new
25
+ end
26
+ end
27
+
28
+ context 'preflight' do
29
+ it 'accepts a valid request' do
30
+ origin = 'http://localhost'
31
+
32
+ options '/v1/test_cors/hello', nil, {
33
+ 'HTTP_ORIGIN' => origin,
34
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
35
+ 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'Content-Type, X-SESSION_ID'
36
+ }
37
+
38
+ expect(last_response.status).to eq(200)
39
+
40
+ expect(last_response.headers['Access-Control-Allow-Origin']).to eq(origin)
41
+ expect(last_response.headers['Access-Control-Allow-Methods']).to eq(Hoodoo::Services::Middleware::ALLOWED_HTTP_METHODS.to_a.join(', '))
42
+ expect(last_response.headers['Access-Control-Allow-Headers']).to eq('Content-Type, X-SESSION_ID')
43
+ end
44
+
45
+ it 'refuses preflight without an Origin header' do
46
+
47
+ # Without an Origin this doesn't look like a CORS request, but in that
48
+ # case the Content-Type 422 will normally get us. Provide a Content-Type
49
+ # here just to make sure that the "invalid HTTP method" code gets a
50
+ # chance to catch it.
51
+
52
+ options '/v1/test_cors/hello', nil, {
53
+ 'CONTENT_TYPE' => 'application/json; charset=utf-8',
54
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET'
55
+ }
56
+
57
+ expect(last_response.status).to eq(405)
58
+ end
59
+
60
+ it 'refuses unsupported methods' do
61
+ options '/v1/test_cors/hello', nil, {
62
+ 'HTTP_ORIGIN' => 'http://localhost',
63
+ 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'PUT' # We use PATCH not PUT
64
+ }
65
+
66
+ expect(last_response.status).to eq(405)
67
+ end
68
+ end
69
+
70
+ context 'other request' do
71
+ it 'quotes the origin' do
72
+ origin = 'http://localhost'
73
+
74
+ get '/v1/test_cors/hello', nil, {
75
+ 'CONTENT_TYPE' => 'application/json; charset=utf-8',
76
+ 'HTTP_ORIGIN' => origin
77
+ }
78
+
79
+ expect(last_response.status).to eq(200)
80
+ expect(last_response.headers['Access-Control-Allow-Origin']).to eq(origin)
81
+ end
82
+
83
+ it 'understands non-CORS requests' do
84
+ get '/v1/test_cors/hello', nil, {
85
+ 'CONTENT_TYPE' => 'application/json; charset=utf-8'
86
+ }
87
+
88
+ expect(last_response.status).to eq(200)
89
+ expect(last_response.headers['Access-Control-Allow-Origin']).to be_nil
90
+ end
91
+ end
92
+
93
+ end
@@ -0,0 +1,489 @@
1
+ # Specific tests for behaviour around the middleware's enforcement of
2
+ # the to_create/to_update DSL through service interfaces and validation
3
+ # of inbound payloads.
4
+ #
5
+ # Since it relies upon the same test setup, the X-Resource-UUID secured
6
+ # header is tested here - both secure headers as a mechanism, and that
7
+ # particular header allowing in a value for a resource ID which gets
8
+ # passed in as an "id" field in the effective body data.
9
+
10
+ require 'spec_helper.rb'
11
+
12
+ # Resource "A" describes different to-create/to-update data as a set
13
+ # of explicit declarations.
14
+
15
+ class RSpecToUpdateToCreateTestAImplementation < Hoodoo::Services::Implementation
16
+ def create( context ); context.response.body = context.request.body; end
17
+ def update( context ); context.response.body = context.request.body; end
18
+ end
19
+
20
+ class RSpecToUpdateToCreateTestAInterface < Hoodoo::Services::Interface
21
+ interface :RSpecToUpdateToCreateTestA do
22
+ endpoint :r_spec_to_update_to_create_test_a, RSpecToUpdateToCreateTestAImplementation
23
+
24
+ # Default values in to-create blocks are irrelevant as they're for
25
+ # rendering only. Expectations in tests for the hashes "seen" by
26
+ # the mock resource implementations check this because the resources
27
+ # return "context.response.body" as-is, so if default values were
28
+ # leaked into that, tests would fail.
29
+
30
+ to_create do
31
+ text :foo
32
+ integer :bar, :required => true
33
+ integer :defaulted, :default => 42
34
+ object :nested do
35
+ text :thing
36
+ end
37
+ end
38
+
39
+ # Required values in to-update blocks are ignored. There is explicit
40
+ # test coverage for this. Default values are handled the same way as
41
+ # with to-create.
42
+
43
+ to_update do
44
+ boolean :foo, :required => true
45
+ enum :bar, :from => [ :foo, :bar, :baz ]
46
+ integer :defaulted, :default => 24
47
+ object :nested do
48
+ text :thing
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # Resource "B" describes to-create/to-update data in terms of Hoodoo
55
+ # Resources and Types. Common resource fields like 'id', 'kind' etc.
56
+ # should still _not_ be permitted for inbound creation or updates.
57
+
58
+ class RSpecToUpdateToCreateTestBImplementation < Hoodoo::Services::Implementation
59
+ def create( context ); context.response.body = context.request.body; end
60
+ def update( context ); context.response.body = context.request.body; end
61
+ end
62
+
63
+ class RSpecToUpdateToCreateTestBInterface < Hoodoo::Services::Interface
64
+ interface :RSpecToUpdateToCreateTestB do
65
+ endpoint :r_spec_to_update_to_create_test_b, RSpecToUpdateToCreateTestBImplementation
66
+
67
+ to_create do
68
+ resource :Errors
69
+ type :ErrorPrimitive
70
+ end
71
+
72
+ to_update do
73
+ resource :Session
74
+ type :Permissions
75
+ end
76
+ end
77
+ end
78
+
79
+ # Resource "C" describes nothing special, to check that common fields
80
+ # are still rejected for creations and updates.
81
+
82
+ class RSpecToUpdateToCreateTestCImplementation < Hoodoo::Services::Implementation
83
+ def create( context ); context.response.body = context.request.body; end
84
+ def update( context ); context.response.body = context.request.body; end
85
+ end
86
+
87
+ class RSpecToUpdateToCreateTestCInterface < Hoodoo::Services::Interface
88
+ interface :RSpecToUpdateToCreateTestC do
89
+ endpoint :r_spec_to_update_to_create_test_c, RSpecToUpdateToCreateTestCImplementation
90
+ end
91
+ end
92
+
93
+ # Put them all in the same service application for simplicity.
94
+
95
+ class RSpecToUpdateToCreateTestService < Hoodoo::Services::Service
96
+ comprised_of RSpecToUpdateToCreateTestAInterface,
97
+ RSpecToUpdateToCreateTestBInterface
98
+
99
+ # Paranoid implicit check that multiple "comprised_of" calls still work :-)
100
+
101
+ comprised_of RSpecToUpdateToCreateTestCInterface
102
+ end
103
+
104
+ # Finally, the tests.
105
+
106
+ describe Hoodoo::Services::Middleware do
107
+ def app
108
+ Rack::Builder.new do
109
+ use Hoodoo::Services::Middleware
110
+ run RSpecToUpdateToCreateTestService.new
111
+ end
112
+ end
113
+
114
+ def do_post( variant, hash, headers = {} ) # Variant is :a or :b
115
+ post "/v1/r_spec_to_update_to_create_test_#{ variant }/",
116
+ hash.nil? ? '' : JSON.generate( hash ),
117
+ { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }.merge( headers )
118
+
119
+ expectations( hash )
120
+ end
121
+
122
+ def do_patch( variant, hash, headers = {} ) # Variant is :a or :b
123
+ patch "/v1/r_spec_to_update_to_create_test_#{ variant }/any",
124
+ hash.nil? ? '' : JSON.generate( hash ),
125
+ { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }.merge( headers )
126
+
127
+ expectations( hash )
128
+ end
129
+
130
+ context 'accepts valid input' do
131
+ def expectations( hash )
132
+ expect( last_response.status ).to eq( 200 )
133
+ expect( JSON.parse( last_response.body ) ).to eq( hash )
134
+ end
135
+
136
+ it 'with many fields' do
137
+ do_post( :a, 'foo' => 'hello', 'bar' => 42 )
138
+ do_patch( :a, 'foo' => true, 'bar' => 'foo' )
139
+ do_post( :b, 'code' => 'hello', 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
140
+ do_patch( :b, 'actions' => { 'list' => 'allow' }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
141
+ end
142
+
143
+ it 'with required fields only' do
144
+ do_post( :a, 'bar' => 42 )
145
+ do_patch( :a, 'foo' => true )
146
+ do_post( :b, 'code' => 'hello', 'message' => 'world', 'errors' => [] )
147
+ do_patch( :b, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
148
+ end
149
+
150
+ # Explicit nils should be preserved through rendering and
151
+ # accepted through the checking engine (for non-required data).
152
+ #
153
+ it 'with explicit nils' do
154
+ do_post( :a, 'foo' => nil, 'bar' => 42 )
155
+ do_patch( :a, 'foo' => true, 'bar' => nil )
156
+ do_post( :b, 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => [] )
157
+ do_patch( :b, 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
158
+ do_patch( :b, 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
159
+ end
160
+ end
161
+
162
+ context 'rejects unknown data' do
163
+ def expectations( hash )
164
+ expect( last_response.status ).to eq( 422 )
165
+ parsed = JSON.parse( last_response.body )
166
+ expect( parsed[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Body data contains unrecognised or prohibited fields' )
167
+ expect( parsed[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'random' )
168
+ end
169
+
170
+ it 'with many fields' do
171
+ do_post( :a, 'foo' => 'hello', 'bar' => 42, 'random' => true )
172
+ do_patch( :a, 'foo' => true, 'bar' => 'foo', 'random' => true )
173
+ do_post( :b, 'code' => 'hello', 'random' => true, 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
174
+ do_patch( :b, 'actions' => { 'list' => 'allow' }, 'random' => true, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
175
+ end
176
+
177
+ it 'with required fields only' do
178
+ do_post( :a, 'bar' => 42, 'random' => true )
179
+ do_patch( :a, 'foo' => true, 'random' => true )
180
+ do_post( :b, 'code' => 'hello', 'random' => true, 'message' => 'world', 'errors' => [] )
181
+ do_patch( :b, 'random' => true, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
182
+ end
183
+
184
+ it 'with explicit nils' do
185
+ do_post( :a, 'foo' => nil, 'bar' => 42, 'random' => true )
186
+ do_patch( :a, 'foo' => true, 'bar' => nil, 'random' => true )
187
+ do_post( :b, 'code' => 'hello', 'random' => true, 'message' => 'world', 'reference' => nil, 'errors' => [] )
188
+ do_patch( :b, 'actions' => nil, 'random' => true, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
189
+ do_patch( :b, 'actions' => { 'list' => nil }, 'random' => true, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
190
+ end
191
+
192
+ it 'rejects unknown data across many fields (flat top level)' do
193
+ def expectations( hash )
194
+ expect( last_response.status ).to eq( 422 )
195
+ parsed = JSON.parse( last_response.body )
196
+ expect( parsed[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Body data contains unrecognised or prohibited fields' )
197
+ expect( parsed[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'random\\, more' )
198
+ end
199
+
200
+ do_post( :a, 'foo' => 'hello', 'bar' => 42, 'random' => { 'foo' => true, 'bar' => true }, 'more' => 42 )
201
+ do_patch( :a, 'foo' => true, 'bar' => 'foo', 'random' => { 'foo' => true, 'bar' => true }, 'more' => 42 )
202
+ end
203
+
204
+ it 'rejects unknown data across many fields (nested top level)' do
205
+ def expectations( hash )
206
+ expect( last_response.status ).to eq( 422 )
207
+ parsed = JSON.parse( last_response.body )
208
+ expect( parsed[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Body data contains unrecognised or prohibited fields' )
209
+ expect( parsed[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'nested.foo\\, nested.bar\\, more' )
210
+ end
211
+
212
+ do_post( :a, 'foo' => 'hello', 'bar' => 42, 'nested' => { 'foo' => true, 'bar' => true }, 'more' => 42 )
213
+ do_patch( :a, 'foo' => true, 'bar' => 'foo', 'nested' => { 'foo' => true, 'bar' => true }, 'more' => 42 )
214
+ end
215
+ end
216
+
217
+ context 'rejects known but prohibited fields' do
218
+ def expectations( hash )
219
+ expect( last_response.status ).to eq( 422 )
220
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Body data contains unrecognised or prohibited fields' )
221
+ end
222
+
223
+ # Paranoia check to ensure rejection when attempting to specify just an ID,
224
+ # with an otherwise entirely valid payload.
225
+ #
226
+ it 'for just "id"' do
227
+ do_post( :a, { 'id' => Hoodoo::UUID.generate, 'foo' => 'hello', 'bar' => 42 } )
228
+ end
229
+
230
+ it 'with many fields' do
231
+ do_post( :b, 'id' => Hoodoo::UUID.generate, 'code' => 'hello', 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
232
+ do_patch( :b, 'id' => Hoodoo::UUID.generate, 'actions' => { 'list' => 'allow' }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
233
+ do_post( :b, 'kind' => 'Foo', 'code' => 'hello', 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
234
+ do_patch( :b, 'kind' => 'Foo', 'actions' => { 'list' => 'allow' }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
235
+ do_post( :b, 'created_at' => Time.now.iso8601, 'code' => 'hello', 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
236
+ do_patch( :b, 'created_at' => Time.now.iso8601, 'actions' => { 'list' => 'allow' }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
237
+ do_post( :b, 'language' => 'fr', 'code' => 'hello', 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
238
+ do_patch( :b, 'language' => 'fr', 'actions' => { 'list' => 'allow' }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
239
+ end
240
+
241
+ it 'with required fields only' do
242
+ do_post( :b, 'id' => Hoodoo::UUID.generate, 'code' => 'hello', 'message' => 'world', 'errors' => [] )
243
+ do_patch( :b, 'id' => Hoodoo::UUID.generate, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
244
+ do_post( :b, 'kind' => 'Foo', 'code' => 'hello', 'message' => 'world', 'errors' => [] )
245
+ do_patch( :b, 'kind' => 'Foo', 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
246
+ do_post( :b, 'created_at' => Time.now.iso8601, 'code' => 'hello', 'message' => 'world', 'errors' => [] )
247
+ do_patch( :b, 'created_at' => Time.now.iso8601, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
248
+ do_post( :b, 'language' => 'fr', 'code' => 'hello', 'message' => 'world', 'errors' => [] )
249
+ do_patch( :b, 'language' => 'fr', 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
250
+ end
251
+
252
+ it 'with explicit nils' do
253
+ do_post( :b, 'id' => Hoodoo::UUID.generate, 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => [] )
254
+ do_patch( :b, 'id' => Hoodoo::UUID.generate, 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
255
+ do_patch( :b, 'id' => Hoodoo::UUID.generate, 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
256
+ do_post( :b, 'kind' => 'Foo', 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => [] )
257
+ do_patch( :b, 'kind' => 'Foo', 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
258
+ do_patch( :b, 'kind' => 'Foo', 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
259
+ do_post( :b, 'created_at' => Time.now.iso8601, 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => [] )
260
+ do_patch( :b, 'created_at' => Time.now.iso8601, 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
261
+ do_patch( :b, 'created_at' => Time.now.iso8601, 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
262
+ do_post( :b, 'language' => 'fr', 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => [] )
263
+ do_patch( :b, 'language' => 'fr', 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
264
+ do_patch( :b, 'language' => 'fr', 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
265
+ end
266
+ end
267
+
268
+ # A more complete range of tests for the DSL is elsewhere, but it's easy
269
+ # to add a bit more coverage here explicitly for the to-create/update code.
270
+
271
+ context 'rejects known fields with incorrect types' do
272
+ def expectations( hash )
273
+ expect( last_response.status ).to eq( 422 )
274
+ end
275
+
276
+ it 'with many fields' do
277
+ do_post( :a, 'foo' => 'hello', 'bar' => 'not an integer' )
278
+ do_patch( :a, 'foo' => 'not boolean', 'bar' => 'foo' )
279
+ do_post( :b, 'code' => 22, 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
280
+ do_patch( :b, 'actions' => 'not an object', 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
281
+ end
282
+
283
+ it 'with required fields only' do
284
+ do_post( :a, 'bar' => 'still not an integer' )
285
+ do_patch( :a, 'foo' => 'still not a boolean' )
286
+ do_post( :b, 'code' => 'hello', 'message' => 'world', 'errors' => 'not an array' )
287
+ do_patch( :b, 'caller_id' => 'not a uuid', 'expires_at' => Time.now.iso8601 )
288
+ end
289
+
290
+ it 'with explicit nils' do
291
+ do_post( :a, 'foo' => nil, 'bar' => 'still not an integer' )
292
+ do_patch( :a, 'foo' => 'still not a boolean', 'bar' => nil )
293
+ do_post( :b, 'code' => 'hello', 'message' => 'world', 'reference' => nil, 'errors' => 'still not an array' )
294
+ do_patch( :b, 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => 'not a time' )
295
+ do_patch( :b, 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => 'not a time' )
296
+ end
297
+ end
298
+
299
+ # Reject common fields where there's no create/update block.
300
+
301
+ context 'rejects common fields without schema' do
302
+ def expectations( ignore )
303
+ expect( last_response.status ).to eq( 422 )
304
+ parsed = JSON.parse( last_response.body )
305
+ expect( parsed[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Body data contains unrecognised or prohibited fields' )
306
+ expect( parsed[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'id\\, created_at\\, kind\\, language' )
307
+ end
308
+
309
+ it 'rejects bad creations' do
310
+ do_post( :c, 'created_at' => 'now', 'id' => '234',
311
+ 'kind' => 'FortyTwo', 'language' => 'bleat',
312
+ 'random' => 'field' )
313
+ end
314
+
315
+ it 'rejects bad updates' do
316
+ do_patch( :c, 'created_at' => 'now', 'id' => '234',
317
+ 'kind' => 'FortyTwo', 'language' => 'bleat',
318
+ 'random' => 'field' )
319
+ end
320
+ end
321
+
322
+ # There's coverage for ":required => true" elsewhere too, but again,
323
+ # may as well add extra coverage for to-create/to-update here.
324
+ #
325
+ # The creation blocks expect to fault the missing required fields with
326
+ # a 422. The update blocks expect this to be OK (omitted field just
327
+ # means "don't change the value").
328
+ #
329
+ context 'required fields' do
330
+ context 'are required for to_create' do
331
+ def expectations( hash )
332
+ expect( last_response.status ).to eq( 422 )
333
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'generic.required_field_missing' )
334
+ end
335
+
336
+ it 'with many fields' do
337
+ do_post( :a, 'foo' => 'hello' )
338
+ do_post( :b, 'message' => 'world', 'reference' => 'baz', 'errors' => [] )
339
+ end
340
+
341
+ it 'with empty data' do
342
+ do_post( :a, {} )
343
+ do_post( :b, {} )
344
+ end
345
+
346
+ it 'with explicit nils' do
347
+ do_post( :a, 'bar' => nil )
348
+ do_post( :b, 'code' => nil, 'message' => 'world', 'reference' => nil, 'errors' => [] )
349
+ end
350
+ end
351
+
352
+ context 'are irrelevant for to_update' do
353
+ def expectations( hash )
354
+ expect( last_response.status ).to eq( 200 )
355
+ end
356
+
357
+ it 'with many fields' do
358
+ do_patch( :a, 'bar' => 'foo' )
359
+ do_patch( :b, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
360
+ end
361
+
362
+ it 'with empty data' do
363
+ do_patch( :a, {} )
364
+ do_patch( :b, {} )
365
+ end
366
+
367
+ it 'with explicit nils' do
368
+ do_patch( :a, 'foo' => nil )
369
+ do_patch( :b, 'actions' => nil, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
370
+ do_patch( :b, 'actions' => { 'list' => nil }, 'caller_id' => Hoodoo::UUID.generate, 'expires_at' => Time.now.iso8601 )
371
+ end
372
+ end
373
+ end
374
+
375
+ # Tests for the X-Resource-UUID header / authorised headers generally.
376
+
377
+ context 'with X-Resource-UUID' do
378
+ before :each do
379
+ @test_uuid = Hoodoo::UUID.generate()
380
+ @old_test_session = Hoodoo::Services::Middleware.test_session()
381
+ @test_session = @old_test_session.dup
382
+ permissions = Hoodoo::Services::Permissions.new # (this is "default-else-deny")
383
+ permissions.set_default_fallback( Hoodoo::Services::Permissions::ALLOW )
384
+ @test_session.permissions = permissions
385
+ @test_session.scoping = @test_session.scoping.dup
386
+ @test_session.scoping.authorised_http_headers = [] # (no secured headers allowed to start with)
387
+ Hoodoo::Services::Middleware.set_test_session( @test_session )
388
+ end
389
+
390
+ after :each do
391
+ Hoodoo::Services::Middleware.set_test_session( @old_test_session )
392
+ end
393
+
394
+ it 'accepts session-authorised and valid IDs' do
395
+ def expectations( hash )
396
+ expect( last_response.status ).to eq( 200 )
397
+ expect( JSON.parse( last_response.body ) ).to eq( hash.merge( 'id' => @test_uuid ) )
398
+ end
399
+
400
+ @test_session.scoping.authorised_http_headers = [ 'HTTP_X_RESOURCE_UUID' ]
401
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => @test_uuid } )
402
+ end
403
+
404
+ # Don't expect any errors for non-POST uses of X-Resource-UUID; just
405
+ # don't expect the body data to contain the 'id' field.
406
+ #
407
+ it 'rejects session-authorised and valid IDs for PATCH' do
408
+ def expectations( hash )
409
+ expect( last_response.status ).to eq( 200 )
410
+ expect( JSON.parse( last_response.body ) ).to eq( { 'foo' => true } )
411
+ end
412
+
413
+ @test_session.scoping.authorised_http_headers = [ 'HTTP_X_RESOURCE_UUID' ]
414
+ do_patch( :a, { 'foo' => true }, { 'HTTP_X_RESOURCE_UUID' => Hoodoo::UUID.generate() } )
415
+ end
416
+
417
+ it 'rejects session-authorised but invalid IDs' do
418
+ def expectations( hash )
419
+ expect( last_response.status ).to eq( 422 )
420
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'reference' ] ).to eq( 'X-Resource-UUID' )
421
+ end
422
+
423
+ @test_session.scoping.authorised_http_headers = [ 'HTTP_X_RESOURCE_UUID' ]
424
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => 'not a valid UUID' } )
425
+ end
426
+
427
+ it 'rejects requests with no authorised headers' do
428
+ def expectations( hash )
429
+ expect( last_response.status ).to eq( 403 )
430
+
431
+ # Check for a *generic* platform.forbidden message. It isn't even very
432
+ # accurate but the whole point is that we don't reveal the exact nature
433
+ # of the authorisation failure (use of a secure header without session
434
+ # permissions) because that would be an information disclosure bug.
435
+ #
436
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Action not authorized' )
437
+ end
438
+
439
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => Hoodoo::UUID.generate } )
440
+ end
441
+
442
+ it 'rejects requests with malformed (nil) authorised header collection' do
443
+ def expectations( hash )
444
+ expect( last_response.status ).to eq( 403 )
445
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Action not authorized' )
446
+ end
447
+
448
+ @test_session.scoping.authorised_http_headers = nil
449
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => Hoodoo::UUID.generate } )
450
+ end
451
+
452
+ it 'rejects requests with an explicitly empty authorised header collection' do
453
+ def expectations( hash )
454
+ expect( last_response.status ).to eq( 403 )
455
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Action not authorized' )
456
+ end
457
+
458
+ @test_session.scoping.authorised_http_headers = []
459
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => Hoodoo::UUID.generate } )
460
+ end
461
+
462
+ it 'rejects requests with mismatched authorised headers' do
463
+ def expectations( hash )
464
+ expect( last_response.status ).to eq( 403 )
465
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'message' ] ).to eq( 'Action not authorized' )
466
+ end
467
+
468
+ @test_session.scoping.authorised_http_headers = [ 'HTTP_NOT_X_RESOURCE_UUID' ]
469
+ do_post( :a, { 'foo' => 'hello', 'bar' => 42 }, { 'HTTP_X_RESOURCE_UUID' => Hoodoo::UUID.generate } )
470
+ end
471
+ end
472
+
473
+ # There's coverage for nil payloads elsewhere as well, but once more,
474
+ # add extra coverage here.
475
+
476
+ context 'edge cases:' do
477
+ def expectations( hash )
478
+ expect( last_response.status ).to eq( 422 )
479
+ expect( JSON.parse( last_response.body )[ 'errors' ][ 0 ][ 'code' ] ).to eq( 'generic.malformed' )
480
+ end
481
+
482
+ it 'no body data' do
483
+ do_post( :a, nil )
484
+ do_patch( :a, nil )
485
+ do_post( :b, nil )
486
+ do_patch( :b, nil )
487
+ end
488
+ end
489
+ end