configuration_service 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,252 @@
1
+ require 'configuration_service'
2
+
3
+ module ConfigurationService
4
+
5
+ module Test
6
+
7
+ ##
8
+ # Abstract Orchestrator provider
9
+ #
10
+ # Extend this class if you want your test orchestration provider to
11
+ # constrain your implementation's interface to work as a configuration
12
+ # service provider. If you have no intention of plugging your
13
+ # implementation into ConfigurationService::Base, build your own test
14
+ # orchestration provider from scratch, using Orchestrator as a guide.
15
+ #
16
+ # Extensions should implement #service_provider, #broken_service_provider,
17
+ # #token_for and #delete_configuration.
18
+ #
19
+ class OrchestrationProvider
20
+
21
+ ##
22
+ # * +:requesting_configurations+
23
+ # * +:publishing_configurations+
24
+ # * +:nothing+ (if possible, provide credentials that don't allow
25
+ # operations on the configuration +identifier+)
26
+ ACTIVITY_ROLE_MAP = {
27
+ :requesting_configurations => :consumer,
28
+ :publishing_configurations => :publisher,
29
+ :nothing => :none
30
+ } unless defined?(ACTIVITY_ROLE_MAP)
31
+ ##
32
+ # * +:consumer+
33
+ # * +:publisher+
34
+ # * +:none+
35
+ #
36
+ ROLES = ACTIVITY_ROLE_MAP.values unless defined?(ROLES)
37
+
38
+ ##
39
+ # Returns a new Orchestrator provider
40
+ #
41
+ # The provider is always initialized with the same configuration +identifier+
42
+ #
43
+ def initialize
44
+ @identifier = "acme"
45
+ end
46
+
47
+ ##
48
+ # See Orchestrator#authorize
49
+ #
50
+ def authorize(role)
51
+ @token = token_for(role)
52
+ end
53
+
54
+ ##
55
+ # See Orchestrator#deauthorize
56
+ #
57
+ def deauthorize
58
+ @token = nil
59
+ end
60
+
61
+ ##
62
+ # See Orchestrator#given_metadata
63
+ #
64
+ def given_metadata
65
+ @metadata = {"version" => "1.0"}
66
+ end
67
+
68
+ ##
69
+ # See Orchestrator#given_metadata_filter
70
+ #
71
+ def given_metadata_filter
72
+ @metadata_filter = {"revision" => "d4d40eab-cf66-4af5-9a77-d956a11682de"}
73
+ end
74
+
75
+ ##
76
+ # See Orchestrator#given_existing_configuration
77
+ #
78
+ def given_existing_configuration
79
+ authorized_as(:publisher) do
80
+ @existing_configuration = publish_configuration
81
+ end
82
+ end
83
+
84
+ ##
85
+ # See Orchestrator#given_invalid_configuration
86
+ #
87
+ def given_invalid_configuration
88
+ @configuration = "This should be an object!"
89
+ end
90
+
91
+ ##
92
+ # See Orchestrator#given_missing_configuration
93
+ #
94
+ def given_missing_configuration
95
+ authorized_as(:publisher) do
96
+ delete_configuration
97
+ @existing_configuration = nil
98
+ end
99
+ end
100
+
101
+ ##
102
+ # See Orchestrator#given_existing_configuration_matching_metadata_filter
103
+ #
104
+ def given_existing_configuration_matching_metadata_filter
105
+ @metadata_matching_filter = (@metadata || {}).merge(@metadata_filter)
106
+ given_existing_configuration
107
+ end
108
+
109
+ ##
110
+ # See Orchestrator#given_existing_configuration_not_matching_metadata_filter
111
+ #
112
+ def given_existing_configuration_not_matching_metadata_filter
113
+ @metadata_matching_filter = {}
114
+ given_existing_configuration
115
+ end
116
+
117
+ ##
118
+ # See Orchestrator#existing_configuration
119
+ #
120
+ def existing_configuration
121
+ @existing_configuration.data
122
+ end
123
+
124
+ ##
125
+ # See Orchestrator#existing_revision
126
+ #
127
+ def existing_revision
128
+ @existing_configuration.revision
129
+ end
130
+
131
+ ##
132
+ # Perform a consuming operation against the service under test
133
+ #
134
+ # The response from the service is wrapped in a test Response.
135
+ #
136
+ def request_configuration
137
+ wrap_response do
138
+ if @metadata_filter
139
+ service.request_configuration(@metadata_filter)
140
+ else
141
+ service.request_configuration
142
+ end
143
+ end
144
+ end
145
+
146
+ ##
147
+ # Perform a publishing operation against the service under test
148
+ #
149
+ # The response from the service is wrapped in a test Response.
150
+ #
151
+ def publish_configuration
152
+ wrap_response do
153
+ if @metadata
154
+ service.publish_configuration(configuration, @metadata)
155
+ elsif @metadata_matching_filter
156
+ service.publish_configuration(configuration, @metadata_matching_filter)
157
+ else
158
+ service.publish_configuration(configuration)
159
+ end
160
+ end
161
+ end
162
+
163
+ ##
164
+ # Arrange for the next publication or consuming operation to fail
165
+ #
166
+ # This is done by using a #broken_service_provider to service the
167
+ # next operation.
168
+ #
169
+ def fail_next_request
170
+ @fail_next = true
171
+ end
172
+
173
+ private
174
+
175
+ ##
176
+ # Return a ConfigurationService::Base provider
177
+ #
178
+ # The provider should use a consistent +identifier+.
179
+ #
180
+ def service_provider # :doc:
181
+ raise NotImplementedError, "#{self.class} must implement service_provider"
182
+ end
183
+
184
+ ##
185
+ # Return a broken ConfigurationService::Base provider
186
+ #
187
+ # The provider's #publish_configuration and #request_configuration
188
+ # methods must raise an Error other than AuthorizationError.
189
+ #
190
+ def broken_service_provider # :doc:
191
+ raise NotImplementedError, "#{self.class} must implement broken_service_provider"
192
+ end
193
+
194
+ ##
195
+ # Delete the configuration identified by the consistent +identifier+
196
+ #
197
+ # Deleting non-existent configuration should not produce an error.
198
+ #
199
+ def delete_configuration # :doc:
200
+ raise NotImplementedError, "#{self.class} must implement delete_configuration"
201
+ end
202
+
203
+ ##
204
+ # Return a token that authorizes +role+
205
+ #
206
+ # Valid roles are:
207
+ #
208
+ # * +:consumer+
209
+ # * +:publisher+
210
+ # * +:nothing+
211
+ #
212
+ # Note that a token should be returned for +:nothing+, but the token should
213
+ # not be authorized to consume or publish to the +identifier+.
214
+ #
215
+ def token_for(role) # :doc:
216
+ raise NotImplementedError, "#{self.class} must implement token_for(role)"
217
+ end
218
+
219
+ def configuration
220
+ @configuration ||= {"verbose" => true}
221
+ end
222
+
223
+ def service
224
+ if @fail_next
225
+ @fail_next = false
226
+ ConfigurationService.new(broken_service_provider)
227
+ else
228
+ ConfigurationService.new(service_provider)
229
+ end
230
+ end
231
+
232
+ def authorized_as(role)
233
+ restore_token = @token
234
+ authorize(role)
235
+ yield.tap do
236
+ @token = restore_token
237
+ end
238
+ end
239
+
240
+ def wrap_response # :nodoc:
241
+ begin
242
+ ConfigurationService::Test::Response::Success.new(yield)
243
+ rescue ConfigurationService::Error => e
244
+ ConfigurationService::Test::Response::Failure.new(e)
245
+ end
246
+ end
247
+
248
+ end
249
+
250
+ end
251
+
252
+ end
@@ -0,0 +1,52 @@
1
+ require "singleton"
2
+
3
+ module ConfigurationService
4
+
5
+ module Test
6
+
7
+ # The Singleton module deletes the instance on include!
8
+ unless defined?(OrchestrationProviderRegistry)
9
+
10
+ ##
11
+ # Singleton registry of Orchestrator providers
12
+ #
13
+ class OrchestrationProviderRegistry
14
+ include Singleton
15
+
16
+ def initialize ## :nodoc:
17
+ @providers = {}
18
+ end
19
+
20
+ ##
21
+ # Register a +provider+ identified by the string +identifier+
22
+ #
23
+ # The +provider+ should be a class with a default (nullary) constructor.
24
+ #
25
+ def register(identifier, provider)
26
+ @providers[identifier] = provider
27
+ end
28
+
29
+ ##
30
+ # Return the +provider+ identified by the string +identifier+
31
+ #
32
+ # The +provider must already have been registered with #register,
33
+ # and should be a class with a default (nullary) constructor.
34
+ #
35
+ # Returns +nil+ if no provider has been registered with the given
36
+ # +identifier+.
37
+ #
38
+ def lookup(identifier)
39
+ @providers[identifier]
40
+ end
41
+
42
+ ##
43
+ # Return the singleton registry instance
44
+ # :singleton-method: instance
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,240 @@
1
+ require 'configuration_service'
2
+
3
+ module ConfigurationService
4
+
5
+ module Test
6
+
7
+ ##
8
+ # The declarative test orchestration API
9
+ #
10
+ # This is the declarative API that describes what must be done to test a
11
+ # configuration service provider. It catalogues all the services that the
12
+ # cucumber step definitions expect from a test orchestration provider.
13
+ #
14
+ # It keeps no domain state, because that would couple it to the
15
+ # service implementation. By making no assumptions at all about the API
16
+ # or data, it allows implementors to produce service implementations
17
+ # that do not adhere to the anticipated ConfigurationService::Base API,
18
+ # by writing their own OrchestrationProvider from scratch.
19
+ #
20
+ # However, implementors who are trying to produce ConfigurationService::Base
21
+ # providers should extend OrchestrationProvider, which anticipates a
22
+ # compatible provider API.
23
+ #
24
+ # Note that the +response+ instance variable is not domain state; it is
25
+ # a test artifact (Response or similar) that orchestration providers use
26
+ # to wrap responses from the service provider.
27
+ #
28
+ class Orchestrator
29
+
30
+ ##
31
+ # Return a new orchestrator initialized with a new instance of +provider_class+.
32
+ #
33
+ # The provider is expected to use a consistent configuration +identifier+ for
34
+ # all publishing and consuming operations.
35
+ #
36
+ def initialize(provider_class)
37
+ @provider = provider_class.new
38
+ end
39
+
40
+ ##
41
+ # Include metadata in the next publishing operation
42
+ #
43
+ def given_metadata
44
+ @provider.given_metadata
45
+ end
46
+
47
+ ##
48
+ # Include a metadata filter in the next consuming operation
49
+ #
50
+ def given_metadata_filter
51
+ @provider.given_metadata_filter
52
+ end
53
+
54
+ ##
55
+ # Arrange a published configuration fixture
56
+ def given_existing_configuration
57
+ @provider.given_existing_configuration
58
+ end
59
+
60
+ ##
61
+ # Use invalid configuration data in the next publishing operation
62
+ #
63
+ def given_invalid_configuration
64
+ @provider.given_invalid_configuration
65
+ end
66
+
67
+ ##
68
+ # Delete any existing configuration
69
+ #
70
+ def given_missing_configuration
71
+ @provider.given_missing_configuration
72
+ end
73
+
74
+ ##
75
+ # Arrange a published configuration fixture
76
+ #
77
+ # Use a metadata filter that matches the fixture's metadata in the next consuming operation.
78
+ #
79
+ def given_existing_configuration_matching_metadata_filter
80
+ @provider.given_existing_configuration_matching_metadata_filter
81
+ end
82
+
83
+ ##
84
+ # Arrange a published configuration fixture
85
+ #
86
+ # Use a metadata filter that does not match the fixture's metadata in the next consuming operation.
87
+ #
88
+ def given_existing_configuration_not_matching_metadata_filter
89
+ @provider.given_existing_configuration_not_matching_metadata_filter
90
+ end
91
+
92
+ ##
93
+ # Return a published configuration fixture
94
+ #
95
+ # E.g. as arranged by #given_existing_configuration.
96
+ #
97
+ # TODO remove; step definitions expect this to be Comparable
98
+ #
99
+ def existing_configuration
100
+ @provider.existing_configuration
101
+ end
102
+
103
+ ##
104
+ # Return the revision of a published configuration fixture
105
+ #
106
+ # E.g. as arranged by #given_existing_configuration.
107
+ #
108
+ # TODO remove; step definitions expect this to be Comparable
109
+ #
110
+ def existing_revision
111
+ @provider.existing_revision
112
+ end
113
+
114
+ ##
115
+ # Authorize the next consuming or publishing operation for +activity+
116
+ #
117
+ # Valid activities (as per ACTIVITY_ROLE_MAP in ConfigurationService::Test::OrchestrationProvider) are:
118
+ #
119
+ # * +:requesting_configurations+
120
+ # * +:publishing_configurations+
121
+ # * +:nothing+
122
+ #
123
+ # Where possible, the orchestration provider should authorize +:nothing+
124
+ # by providing valid credentials that don't allow operations on the
125
+ # configuration +identifier+ that it tests against.
126
+ #
127
+ def authorize(activity)
128
+ role = role_for(activity) or raise "unknown authorizable activity #{activity.inspect}"
129
+ @provider.authorize(role)
130
+ end
131
+
132
+ ##
133
+ # Remove any previous authorization
134
+ #
135
+ # E.g. as arranged by #authorize.
136
+ #
137
+ def deauthorize
138
+ @provider.deauthorize
139
+ end
140
+
141
+ ##
142
+ # Perform a consuming operation against the service under test
143
+ #
144
+ # The provider is expected to wrap the response in a Response (or
145
+ # simimlar) and return that.
146
+ #
147
+ def request_configuration
148
+ @response = @provider.request_configuration
149
+ end
150
+
151
+ ##
152
+ # Perform a publishing operation against the service under test
153
+ #
154
+ # The provider is expected to wrap the response in a Response (or
155
+ # simimlar) and return that.
156
+ #
157
+ def publish_configuration
158
+ @response = @provider.publish_configuration
159
+ end
160
+
161
+ ##
162
+ # True if the last consuming or publishing operation was allowed
163
+ #
164
+ def request_allowed?
165
+ @response.allowed?
166
+ end
167
+
168
+ ##
169
+ # True if the last consuming or publishing operation failed
170
+ #
171
+ # Operations that were not allowed (as per #request_allowed?) or
172
+ # considered failed.
173
+ #
174
+ def request_failed?
175
+ @response.failed?
176
+ end
177
+
178
+ ##
179
+ # True if the last consuming operation did not return data
180
+ #
181
+ def request_not_found?
182
+ not @response.found?
183
+ end
184
+
185
+ ##
186
+ # True if the last consuming operation did not return data
187
+ #
188
+ # TODO: distinguish #request_not_matched? to mean "found data, but filtered out by metadata filter"
189
+ def request_not_matched?
190
+ not @response.found?
191
+ end
192
+
193
+ ##
194
+ # The last published or consumed configuration data
195
+ #
196
+ # Note that this is the data itself, not a Configuration object.
197
+ #
198
+ def published_configuration
199
+ @response.data
200
+ end
201
+ alias :requested_configuration :published_configuration
202
+
203
+ ##
204
+ # The revision of the last published or consumed configuration
205
+ #
206
+ def published_revision
207
+ @response.revision
208
+ end
209
+
210
+ ##
211
+ # The last published metadata
212
+ #
213
+ def published_metadata
214
+ @response.metadata
215
+ end
216
+
217
+ ##
218
+ # Arrange for the next publication operation to fail
219
+ #
220
+ def given_publication_failure
221
+ @provider.fail_next_request
222
+ end
223
+
224
+ ##
225
+ # Arrange for the next consuming operation to fail
226
+ #
227
+ def given_request_failure
228
+ @provider.fail_next_request
229
+ end
230
+
231
+ private
232
+
233
+ def role_for(activity)
234
+ ConfigurationService::Test::OrchestrationProvider::ACTIVITY_ROLE_MAP[activity]
235
+ end
236
+ end
237
+
238
+ end
239
+
240
+ end
@@ -0,0 +1,31 @@
1
+ module ConfigurationService
2
+
3
+ module Test
4
+
5
+ ##
6
+ # Builds an Orchestrator using a provider selected from the environment
7
+ #
8
+ module OrchestratorEnvironmentFactory
9
+
10
+ ##
11
+ # Looks up the provider registered to the OrchestrationProviderRegistry
12
+ # with the name provided in the +TEST_ORCHESTRATION_PROVIDER+ environment
13
+ # variable, and returns a new Orchestrator initialized with that
14
+ # provider.
15
+ #
16
+ # Returns a new Orchestrator, or raises a +RuntimeError+ if the
17
+ # +TEST_ORCHESTRATION_PROVIDER+ environment variable does not name a
18
+ # provider known to the OrchestrationProviderRegistry.
19
+ #
20
+ def self.build
21
+ identifier = ENV["TEST_ORCHESTRATION_PROVIDER"] or raise "missing environment variable: TEST_ORCHESTRATION_PROVIDER"
22
+ registry = ConfigurationService::Test::OrchestrationProviderRegistry.instance
23
+ provider = registry.lookup(identifier) or raise "unknown test orchestration provider: #{identifier}"
24
+ @test = ConfigurationService::Test::Orchestrator.new(provider)
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,141 @@
1
+ require "configuration_service"
2
+
3
+ module ConfigurationService
4
+
5
+ module Test
6
+
7
+ ##
8
+ # Encapsulation of ConfigurationService::Base responses
9
+ #
10
+ # See Success and Failure.
11
+ #
12
+ module Response
13
+
14
+ ##
15
+ # Encapsulates a non-error ConfigurationService::Base response
16
+ #
17
+ # This allows an OrchestrationProvider to decouple the Orchestrator
18
+ # from the implementation details of the service provider's responses.
19
+ #
20
+ class Success
21
+
22
+ ##
23
+ # Initialize a new Success with a response
24
+ #
25
+ # The response should be a Configuration object or +nil+ (because
26
+ # the ConfigurationService::Base API communicates +not found+ as
27
+ # +nil+).
28
+ #
29
+ def initialize(response)
30
+ @response = response
31
+ end
32
+
33
+ ##
34
+ # Always true
35
+ #
36
+ def allowed?
37
+ true
38
+ end
39
+
40
+ ##
41
+ # Always false
42
+ #
43
+ def failed?
44
+ false
45
+ end
46
+
47
+ ##
48
+ # True if the +response+ was not +nil+
49
+ #
50
+ def found?
51
+ not @response.nil?
52
+ end
53
+
54
+ ##
55
+ # The configuration data dictionary of the response, or +nil+ if not #found?
56
+ #
57
+ def data
58
+ @response and @response.data
59
+ end
60
+
61
+ ##
62
+ # The revision from the response's metadata, or +nil+ if not #found?
63
+ #
64
+ def revision
65
+ @response and @response.revision
66
+ end
67
+
68
+ ##
69
+ # The metadata dictionary of the response, or +nil+ if not #found?
70
+ #
71
+ def metadata
72
+ @response and @response.metadata
73
+ end
74
+
75
+ end
76
+
77
+ ##
78
+ # Encapsulates an error ConfigurationService::Base response
79
+ #
80
+ # This allows an OrchestrationProvider to decouple the Orchestrator
81
+ # from the implementation details of the service provider's error
82
+ # handling.
83
+ #
84
+ class Failure
85
+
86
+ ##
87
+ # Initialize a new Failure with an exception
88
+ #
89
+ def initialize(exception)
90
+ @exception = exception
91
+ end
92
+
93
+ ##
94
+ # True unless the exception was an AuthorizationError
95
+ #
96
+ def allowed?
97
+ !@exception.is_a?(ConfigurationService::AuthorizationError)
98
+ end
99
+
100
+ ##
101
+ # True if the exception was an Error but not an AuthorizationError
102
+ #
103
+ def failed?
104
+ allowed? and @exception.is_a?(ConfigurationService::Error)
105
+ end
106
+
107
+ ##
108
+ # Always false
109
+ #
110
+ def found?
111
+ false
112
+ end
113
+
114
+ ##
115
+ # Raises +NotImplementedError+
116
+ #
117
+ def data
118
+ raise NotImplementedError, "configuration not available after #{@exception.inspect}"
119
+ end
120
+
121
+ ##
122
+ # Raises +NotImplementedError+
123
+ #
124
+ def revision
125
+ raise NotImplementedError, "revision not available after #{@exception.inspect}"
126
+ end
127
+
128
+ ##
129
+ # Raises +NotImplementedError+
130
+ #
131
+ def metadata
132
+ raise NotImplementedError, "metadata not available after #{@exception.inspect}"
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end