copycopter_client 1.0.0.beta1

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 (38) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +71 -0
  3. data/Rakefile +38 -0
  4. data/features/rails.feature +267 -0
  5. data/features/step_definitions/copycopter_server_steps.rb +65 -0
  6. data/features/step_definitions/rails_steps.rb +134 -0
  7. data/features/support/env.rb +8 -0
  8. data/features/support/rails_server.rb +118 -0
  9. data/init.rb +2 -0
  10. data/lib/copycopter_client/client.rb +117 -0
  11. data/lib/copycopter_client/configuration.rb +197 -0
  12. data/lib/copycopter_client/errors.rb +13 -0
  13. data/lib/copycopter_client/helper.rb +40 -0
  14. data/lib/copycopter_client/i18n_backend.rb +100 -0
  15. data/lib/copycopter_client/prefixed_logger.rb +41 -0
  16. data/lib/copycopter_client/rails.rb +31 -0
  17. data/lib/copycopter_client/railtie.rb +13 -0
  18. data/lib/copycopter_client/sync.rb +145 -0
  19. data/lib/copycopter_client/version.rb +8 -0
  20. data/lib/copycopter_client.rb +58 -0
  21. data/lib/tasks/copycopter_client_tasks.rake +6 -0
  22. data/spec/copycopter_client/client_spec.rb +208 -0
  23. data/spec/copycopter_client/configuration_spec.rb +252 -0
  24. data/spec/copycopter_client/helper_spec.rb +86 -0
  25. data/spec/copycopter_client/i18n_backend_spec.rb +133 -0
  26. data/spec/copycopter_client/prefixed_logger_spec.rb +25 -0
  27. data/spec/copycopter_client/sync_spec.rb +295 -0
  28. data/spec/spec.opts +2 -0
  29. data/spec/spec_helper.rb +30 -0
  30. data/spec/support/client_spec_helpers.rb +9 -0
  31. data/spec/support/defines_constants.rb +38 -0
  32. data/spec/support/fake_client.rb +42 -0
  33. data/spec/support/fake_copycopter_app.rb +136 -0
  34. data/spec/support/fake_html_safe_string.rb +20 -0
  35. data/spec/support/fake_logger.rb +68 -0
  36. data/spec/support/fake_passenger.rb +27 -0
  37. data/spec/support/fake_unicorn.rb +14 -0
  38. metadata +121 -0
@@ -0,0 +1,145 @@
1
+ require 'thread'
2
+ require 'copycopter_client/client'
3
+
4
+ module CopycopterClient
5
+ # Manages synchronization of copy between {I18nBackend} and {Client}. Acts
6
+ # like a Hash. Applications using the client will not need to interact with
7
+ # this class directly.
8
+ #
9
+ # Responsible for:
10
+ # * Starting and running the background polling thread
11
+ # * Locking down access to data used by both threads
12
+ class Sync
13
+ # Usually instantiated when {Configuration#apply} is invoked.
14
+ # @param client [Client] the client used to fetch and upload data
15
+ # @param options [Hash]
16
+ # @option options [Fixnum] :polling_delay the number of seconds in between each synchronization with the server
17
+ # @option options [Logger] :logger where errors should be logged
18
+ def initialize(client, options)
19
+ @client = client
20
+ @blurbs = {}
21
+ @polling_delay = options[:polling_delay]
22
+ @stop = false
23
+ @queued = {}
24
+ @mutex = Mutex.new
25
+ @logger = options[:logger]
26
+ @pending = false
27
+ end
28
+
29
+ # Starts the polling thread. The polling thread doesn't run in test environments.
30
+ #
31
+ # If this sync was created from a master spawner (as in the case for
32
+ # phusion passenger), it will instead register after fork hooks so that the
33
+ # poller starts in each spawned process.
34
+ def start
35
+ if spawner?
36
+ register_spawn_hooks
37
+ else
38
+ logger.info("Starting poller")
39
+ @pending = true
40
+ at_exit { sync }
41
+ Thread.new { poll }
42
+ end
43
+ end
44
+
45
+ # Stops the polling thread after the next run.
46
+ def stop
47
+ @stop = true
48
+ end
49
+
50
+ # Returns content for the given blurb.
51
+ # @param key [String] the key of the desired blurb
52
+ # @return [String] the contents of the blurb
53
+ def [](key)
54
+ lock { @blurbs[key] }
55
+ end
56
+
57
+ # Sets content for the given blurb. The content will be pushed to the
58
+ # server on the next poll.
59
+ # @param key [String] the key of the blurb to update
60
+ # @param value [String] the new contents of the blurb
61
+ def []=(key, value)
62
+ lock { @queued[key] = value }
63
+ end
64
+
65
+ # Keys for all blurbs stored on the server.
66
+ # @return [Array<String>] keys
67
+ def keys
68
+ lock { @blurbs.keys }
69
+ end
70
+
71
+ # Waits until the first download has finished.
72
+ def wait_for_download
73
+ if @pending
74
+ logger.info("Waiting for first sync")
75
+ while @pending
76
+ sleep(0.1)
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :client, :polling_delay, :logger
84
+
85
+ def poll
86
+ until @stop
87
+ sync
88
+ sleep(polling_delay)
89
+ end
90
+ rescue InvalidApiKey => error
91
+ logger.error(error.message)
92
+ end
93
+
94
+ def sync
95
+ begin
96
+ downloaded_blurbs = client.download
97
+ lock { @blurbs = downloaded_blurbs }
98
+ with_queued_changes do |queued|
99
+ client.upload(queued)
100
+ end
101
+ rescue ConnectionError => error
102
+ logger.error(error.message)
103
+ end
104
+ ensure
105
+ @pending = false
106
+ end
107
+
108
+ def with_queued_changes
109
+ changes_to_push = nil
110
+ lock do
111
+ unless @queued.empty?
112
+ changes_to_push = @queued
113
+ @queued = {}
114
+ end
115
+ end
116
+ yield(changes_to_push) if changes_to_push
117
+ end
118
+
119
+ def lock(&block)
120
+ @mutex.synchronize(&block)
121
+ end
122
+
123
+ def spawner?
124
+ $0.include?("ApplicationSpawner") || $0 =~ /unicorn.*master/
125
+ end
126
+
127
+ def register_spawn_hooks
128
+ if defined?(PhusionPassenger)
129
+ logger.info("Registered Phusion Passenger fork hook")
130
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
131
+ start
132
+ end
133
+ elsif defined?(Unicorn::HttpServer)
134
+ logger.info("Registered Unicorn fork hook")
135
+ Unicorn::HttpServer.class_eval do
136
+ alias_method :worker_loop_without_copycopter, :worker_loop
137
+ def worker_loop(worker)
138
+ CopycopterClient.start_sync
139
+ worker_loop_without_copycopter(worker)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,8 @@
1
+ module CopycopterClient
2
+ # Client version
3
+ VERSION = "1.0.0.beta1"
4
+
5
+ # API version being used to communicate with the server
6
+ API_VERSION = "2.0"
7
+ end
8
+
@@ -0,0 +1,58 @@
1
+ require 'copycopter_client/version'
2
+ require 'copycopter_client/configuration'
3
+
4
+ # Top-level interface to the Copycopter client.
5
+ #
6
+ # Most applications should only need to use the {.configure}
7
+ # method, which will setup all the pieces and begin synchronization when
8
+ # appropriate.
9
+ module CopycopterClient
10
+ class << self
11
+ # @return [Client] instance used to communicate with the Copycopter server.
12
+ # This is set when {.configure} is called.
13
+ attr_accessor :client
14
+
15
+ # @return [Configuration] current client configuration
16
+ # Must act like a hash and return sensible values for all Copycopter
17
+ # configuration options. Usually set when {.configure} is called.
18
+ attr_accessor :configuration
19
+
20
+ # @return [Sync] instance used to synchronize changes.
21
+ # This is set when {.configure} is called.
22
+ attr_accessor :sync
23
+ end
24
+
25
+ # Issues a new deploy, marking all draft blurbs as published.
26
+ # This is called when the copycopter:deploy rake task is invoked.
27
+ def self.deploy
28
+ client.deploy
29
+ end
30
+
31
+ # Starts the polling process.
32
+ # This is called from Unicorn worker processes.
33
+ def self.start_sync
34
+ sync.start
35
+ end
36
+
37
+ # Call this method to modify defaults in your initializers.
38
+ #
39
+ # @example
40
+ # CopycopterClient.configure do |config|
41
+ # config.api_key = '1234567890abcdef'
42
+ # config.secure = false
43
+ # end
44
+ #
45
+ # @param apply [Boolean] (internal) whether the configuration should be applied yet.
46
+ #
47
+ # @yield [Configuration] the configuration to be modified
48
+ def self.configure(apply = true)
49
+ self.configuration ||= Configuration.new
50
+ yield(configuration)
51
+ configuration.apply if apply
52
+ end
53
+ end
54
+
55
+ if defined?(Rails)
56
+ require 'copycopter_client/rails'
57
+ end
58
+
@@ -0,0 +1,6 @@
1
+ namespace :copycopter do
2
+ desc "Notify Copycopter of a new deploy."
3
+ task :deploy => :environment do
4
+ CopycopterClient.deploy
5
+ end
6
+ end
@@ -0,0 +1,208 @@
1
+ require 'spec_helper'
2
+
3
+ describe CopycopterClient do
4
+ def build_client(config = {})
5
+ config[:logger] ||= FakeLogger.new
6
+ default_config = CopycopterClient::Configuration.new.to_hash
7
+ CopycopterClient::Client.new(default_config.update(config))
8
+ end
9
+
10
+ def add_project
11
+ api_key = 'xyz123'
12
+ FakeCopycopterApp.add_project(api_key)
13
+ end
14
+
15
+ def build_client_with_project(config = {})
16
+ project = add_project
17
+ config[:api_key] = project.api_key
18
+ build_client(config)
19
+ end
20
+
21
+ describe "opening a connection" do
22
+ let(:config) { CopycopterClient::Configuration.new }
23
+ let(:http) { Net::HTTP.new(config.host, config.port) }
24
+
25
+ before do
26
+ Net::HTTP.stubs(:new => http)
27
+ end
28
+
29
+ it "should timeout when connecting" do
30
+ project = add_project
31
+ client = build_client(:api_key => project.api_key, :http_open_timeout => 4)
32
+ client.download
33
+ http.open_timeout.should == 4
34
+ end
35
+
36
+ it "should timeout when reading" do
37
+ project = add_project
38
+ client = build_client(:api_key => project.api_key, :http_read_timeout => 4)
39
+ client.download
40
+ http.read_timeout.should == 4
41
+ end
42
+
43
+ it "uses ssl when secure" do
44
+ project = add_project
45
+ client = build_client(:api_key => project.api_key, :secure => true)
46
+ client.download
47
+ http.use_ssl.should == true
48
+ end
49
+
50
+ it "doesn't use ssl when insecure" do
51
+ project = add_project
52
+ client = build_client(:api_key => project.api_key, :secure => false)
53
+ client.download
54
+ http.use_ssl.should == false
55
+ end
56
+
57
+ it "wraps HTTP errors with ConnectionError" do
58
+ errors = [
59
+ Timeout::Error.new,
60
+ Errno::EINVAL.new,
61
+ Errno::ECONNRESET.new,
62
+ EOFError.new,
63
+ Net::HTTPBadResponse.new,
64
+ Net::HTTPHeaderSyntaxError.new,
65
+ Net::ProtocolError.new
66
+ ]
67
+
68
+ errors.each do |original_error|
69
+ http.stubs(:get).raises(original_error)
70
+ client = build_client_with_project
71
+ expect { client.download }.
72
+ to raise_error(CopycopterClient::ConnectionError) { |error|
73
+ error.message.
74
+ should == "#{original_error.class.name}: #{original_error.message}"
75
+ }
76
+ end
77
+ end
78
+
79
+ it "handles 500 errors from downloads with ConnectionError" do
80
+ client = build_client(:api_key => 'raise_error')
81
+ expect { client.download }.to raise_error(CopycopterClient::ConnectionError)
82
+ end
83
+
84
+ it "handles 500 errors from uploads with ConnectionError" do
85
+ client = build_client(:api_key => 'raise_error')
86
+ expect { client.upload({}) }.to raise_error(CopycopterClient::ConnectionError)
87
+ end
88
+
89
+ it "handles 404 errors from downloads with ConnectionError" do
90
+ client = build_client(:api_key => 'bogus')
91
+ expect { client.download }.to raise_error(CopycopterClient::InvalidApiKey)
92
+ end
93
+
94
+ it "handles 404 errors from uploads with ConnectionError" do
95
+ client = build_client(:api_key => 'bogus')
96
+ expect { client.upload({}) }.to raise_error(CopycopterClient::InvalidApiKey)
97
+ end
98
+ end
99
+
100
+ it "downloads published blurbs for an existing project" do
101
+ project = add_project
102
+ project.update({
103
+ 'draft' => {
104
+ 'key.one' => "unexpected one",
105
+ 'key.three' => "unexpected three"
106
+ },
107
+ 'published' => {
108
+ 'key.one' => "expected one",
109
+ 'key.two' => "expected two"
110
+ }
111
+ })
112
+
113
+ blurbs = build_client(:api_key => project.api_key, :public => true).download
114
+
115
+ blurbs.should == {
116
+ 'key.one' => 'expected one',
117
+ 'key.two' => 'expected two'
118
+ }
119
+ end
120
+
121
+ it "logs that it performed a download" do
122
+ logger = FakeLogger.new
123
+ client = build_client_with_project(:logger => logger)
124
+ client.download
125
+ logger.should have_entry(:info, "Downloaded translations")
126
+ end
127
+
128
+ it "downloads draft blurbs for an existing project" do
129
+ project = add_project
130
+ project.update({
131
+ 'draft' => {
132
+ 'key.one' => "expected one",
133
+ 'key.two' => "expected two"
134
+ },
135
+ 'published' => {
136
+ 'key.one' => "unexpected one",
137
+ 'key.three' => "unexpected three"
138
+ }
139
+ })
140
+
141
+ blurbs = build_client(:api_key => project.api_key, :public => false).download
142
+
143
+ blurbs.should == {
144
+ 'key.one' => 'expected one',
145
+ 'key.two' => 'expected two'
146
+ }
147
+ end
148
+
149
+ it "uploads defaults for missing blurbs in an existing project" do
150
+ project = add_project
151
+
152
+ blurbs = {
153
+ 'key.one' => 'expected one',
154
+ 'key.two' => 'expected two'
155
+ }
156
+
157
+ client = build_client(:api_key => project.api_key, :public => true)
158
+ client.upload(blurbs)
159
+
160
+ project.reload.draft.should == blurbs
161
+ end
162
+
163
+ it "logs that it performed an upload" do
164
+ logger = FakeLogger.new
165
+ client = build_client_with_project(:logger => logger)
166
+ client.upload({})
167
+ logger.should have_entry(:info, "Uploaded missing translations")
168
+ end
169
+
170
+ it "deploys from the top-level constant" do
171
+ client = build_client
172
+ CopycopterClient.client = client
173
+ client.stubs(:deploy)
174
+
175
+ CopycopterClient.deploy
176
+
177
+ client.should have_received(:deploy)
178
+ end
179
+
180
+ it "deploys" do
181
+ project = add_project
182
+ project.update({
183
+ 'draft' => {
184
+ 'key.one' => "expected one",
185
+ 'key.two' => "expected two"
186
+ },
187
+ 'published' => {
188
+ 'key.one' => "unexpected one",
189
+ 'key.two' => "unexpected one",
190
+ }
191
+ })
192
+ logger = FakeLogger.new
193
+ client = build_client(:api_key => project.api_key, :logger => logger)
194
+
195
+ client.deploy
196
+
197
+ project.reload.published.should == {
198
+ 'key.one' => "expected one",
199
+ 'key.two' => "expected two"
200
+ }
201
+ logger.should have_entry(:info, "Deployed")
202
+ end
203
+
204
+ it "handles deploy errors" do
205
+ expect { build_client.deploy }.to raise_error(CopycopterClient::InvalidApiKey)
206
+ end
207
+ end
208
+
@@ -0,0 +1,252 @@
1
+ require 'spec_helper'
2
+
3
+ describe CopycopterClient::Configuration do
4
+ Spec::Matchers.define :have_config_option do |option|
5
+ match do |config|
6
+ config.should respond_to(option)
7
+
8
+ if instance_variables.include?('@default')
9
+ config.send(option).should == @default
10
+ end
11
+
12
+ if @overridable
13
+ value = 'a value'
14
+ config.send(:"#{option}=", value)
15
+ config.send(option).should == value
16
+ end
17
+ end
18
+
19
+ chain :default do |default|
20
+ @default = default
21
+ end
22
+
23
+ chain :overridable do
24
+ @overridable = true
25
+ end
26
+ end
27
+
28
+ it { should have_config_option(:proxy_host). overridable.default(nil) }
29
+ it { should have_config_option(:proxy_port). overridable.default(nil) }
30
+ it { should have_config_option(:proxy_user). overridable.default(nil) }
31
+ it { should have_config_option(:proxy_pass). overridable.default(nil) }
32
+ it { should have_config_option(:environment_name). overridable.default(nil) }
33
+ it { should have_config_option(:client_version). overridable.default(CopycopterClient::VERSION) }
34
+ it { should have_config_option(:client_name). overridable.default('Copycopter Client') }
35
+ it { should have_config_option(:client_url). overridable.default('http://copycopter.com') }
36
+ it { should have_config_option(:secure). overridable.default(false) }
37
+ it { should have_config_option(:host). overridable.default('copycopter.com') }
38
+ it { should have_config_option(:http_open_timeout). overridable.default(2) }
39
+ it { should have_config_option(:http_read_timeout). overridable.default(5) }
40
+ it { should have_config_option(:port). overridable }
41
+ it { should have_config_option(:development_environments).overridable }
42
+ it { should have_config_option(:api_key). overridable }
43
+ it { should have_config_option(:polling_delay). overridable.default(300) }
44
+ it { should have_config_option(:framework). overridable }
45
+ it { should have_config_option(:fallback_backend). overridable }
46
+
47
+ it "should provide default values for secure connections" do
48
+ config = CopycopterClient::Configuration.new
49
+ config.secure = true
50
+ config.port.should == 443
51
+ config.protocol.should == 'https'
52
+ end
53
+
54
+ it "should provide default values for insecure connections" do
55
+ config = CopycopterClient::Configuration.new
56
+ config.secure = false
57
+ config.port.should == 80
58
+ config.protocol.should == 'http'
59
+ end
60
+
61
+ it "should not cache inferred ports" do
62
+ config = CopycopterClient::Configuration.new
63
+ config.secure = false
64
+ config.port
65
+ config.secure = true
66
+ config.port.should == 443
67
+ end
68
+
69
+ it "should act like a hash" do
70
+ config = CopycopterClient::Configuration.new
71
+ hash = config.to_hash
72
+ [:api_key, :environment_name, :host, :http_open_timeout,
73
+ :http_read_timeout, :client_name, :client_url, :client_version, :port,
74
+ :protocol, :proxy_host, :proxy_pass, :proxy_port, :proxy_user, :secure,
75
+ :development_environments, :logger, :framework, :fallback_backend].each do |option|
76
+ hash[option].should == config[option]
77
+ end
78
+ hash[:public].should == config.public?
79
+ end
80
+
81
+ it "should be mergable" do
82
+ config = CopycopterClient::Configuration.new
83
+ hash = config.to_hash
84
+ config.merge(:key => 'value').should == hash.merge(:key => 'value')
85
+ end
86
+
87
+ it "should use development and staging as development environments by default" do
88
+ config = CopycopterClient::Configuration.new
89
+ config.development_environments.should =~ %w(development staging)
90
+ end
91
+
92
+ it "should use test and cucumber as test environments by default" do
93
+ config = CopycopterClient::Configuration.new
94
+ config.test_environments.should =~ %w(test cucumber)
95
+ end
96
+
97
+ it "should be test in a test environment" do
98
+ config = CopycopterClient::Configuration.new
99
+ config.test_environments = %w(test)
100
+ config.environment_name = 'test'
101
+ config.should be_test
102
+ end
103
+
104
+ it "should be public in a public environment" do
105
+ config = CopycopterClient::Configuration.new
106
+ config.development_environments = %w(development)
107
+ config.environment_name = 'production'
108
+ config.should be_public
109
+ end
110
+
111
+ it "should not be public in a development environment" do
112
+ config = CopycopterClient::Configuration.new
113
+ config.development_environments = %w(staging)
114
+ config.environment_name = 'staging'
115
+ config.should_not be_public
116
+ end
117
+
118
+ it "should be public without an environment name" do
119
+ config = CopycopterClient::Configuration.new
120
+ config.should be_public
121
+ end
122
+
123
+ it "should yield and save a configuration when configuring" do
124
+ yielded_configuration = nil
125
+ CopycopterClient.configure(false) do |config|
126
+ yielded_configuration = config
127
+ end
128
+
129
+ yielded_configuration.should be_kind_of(CopycopterClient::Configuration)
130
+ CopycopterClient.configuration.should == yielded_configuration
131
+ end
132
+
133
+ it "doesn't apply the configuration when asked not to" do
134
+ logger = FakeLogger.new
135
+ CopycopterClient.configure(false) { |config| config.logger = logger }
136
+ CopycopterClient.configuration.should_not be_applied
137
+ logger.entries[:info].should be_empty
138
+ end
139
+
140
+ it "should not remove existing config options when configuring twice" do
141
+ first_config = nil
142
+ CopycopterClient.configure(false) do |config|
143
+ first_config = config
144
+ end
145
+ CopycopterClient.configure(false) do |config|
146
+ config.should == first_config
147
+ end
148
+ end
149
+
150
+ it "starts out unapplied" do
151
+ CopycopterClient::Configuration.new.should_not be_applied
152
+ end
153
+
154
+ it "logs to $stdout by default" do
155
+ logger = FakeLogger.new
156
+ Logger.stubs(:new => logger)
157
+
158
+ config = CopycopterClient::Configuration.new
159
+ Logger.should have_received(:new).with($stdout)
160
+ config.logger.original_logger.should == logger
161
+ end
162
+
163
+ it "generates environment info without a framework" do
164
+ subject.environment_name = 'production'
165
+ subject.environment_info.should == "[Ruby: #{RUBY_VERSION}] [Env: production]"
166
+ end
167
+
168
+ it "generates environment info with a framework" do
169
+ subject.environment_name = 'production'
170
+ subject.framework = 'Sinatra: 1.0.0'
171
+ subject.environment_info.
172
+ should == "[Ruby: #{RUBY_VERSION}] [Sinatra: 1.0.0] [Env: production]"
173
+ end
174
+
175
+ it "prefixes log entries" do
176
+ logger = FakeLogger.new
177
+ config = CopycopterClient::Configuration.new
178
+
179
+ config.logger = logger
180
+
181
+ prefixed_logger = config.logger
182
+ prefixed_logger.should be_a(CopycopterClient::PrefixedLogger)
183
+ prefixed_logger.original_logger.should == logger
184
+ end
185
+ end
186
+
187
+ share_examples_for "applied configuration" do
188
+ let(:backend) { stub('i18n-backend') }
189
+ let(:sync) { stub('sync', :start => nil) }
190
+ let(:client) { stub('client') }
191
+ let(:logger) { FakeLogger.new }
192
+ subject { CopycopterClient::Configuration.new }
193
+
194
+ before do
195
+ CopycopterClient::I18nBackend.stubs(:new => backend)
196
+ CopycopterClient::Client.stubs(:new => client)
197
+ CopycopterClient::Sync.stubs(:new => sync)
198
+ subject.logger = logger
199
+ end
200
+
201
+ it { should be_applied }
202
+
203
+ it "builds and assigns an I18n backend" do
204
+ CopycopterClient::Client.should have_received(:new).with(subject.to_hash)
205
+ CopycopterClient::Sync.should have_received(:new).with(client, subject.to_hash)
206
+ CopycopterClient::I18nBackend.should have_received(:new).with(sync, subject.to_hash)
207
+ I18n.backend.should == backend
208
+ end
209
+
210
+ it "logs that it's ready" do
211
+ logger.should have_entry(:info, "Client #{CopycopterClient::VERSION} ready")
212
+ end
213
+
214
+ it "logs environment info" do
215
+ logger.should have_entry(:info, "Environment Info: #{subject.environment_info}")
216
+ end
217
+
218
+ it "stores the client" do
219
+ CopycopterClient.client.should == client
220
+ end
221
+
222
+ it "stores the sync" do
223
+ CopycopterClient.sync.should == sync
224
+ end
225
+ end
226
+
227
+ describe CopycopterClient::Configuration, "applied when testing" do
228
+ it_should_behave_like "applied configuration"
229
+
230
+ before do
231
+ subject.environment_name = 'test'
232
+ subject.apply
233
+ end
234
+
235
+ it "doesn't start sync" do
236
+ sync.should have_received(:start).never
237
+ end
238
+ end
239
+
240
+ describe CopycopterClient::Configuration, "applied when not testing" do
241
+ it_should_behave_like "applied configuration"
242
+
243
+ before do
244
+ subject.environment_name = 'development'
245
+ subject.apply
246
+ end
247
+
248
+ it "starts sync" do
249
+ sync.should have_received(:start)
250
+ end
251
+ end
252
+