configuration_service 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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