copycopter_client 1.0.0.beta1

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