hoodoo 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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