subscribed_to 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +1 -0
  2. data/Gemfile.lock +2 -0
  3. data/README.rdoc +32 -0
  4. data/app/controllers/subscribed_to/mail_chimp_web_hooks_controller.rb +19 -0
  5. data/config/routes.rb +5 -0
  6. data/lib/active_record_extensions.rb +10 -0
  7. data/lib/generators/subscribed_to/templates/migration.rb +8 -0
  8. data/lib/generators/subscribed_to/templates/subscribed_to.rb +4 -0
  9. data/lib/subscribed_to/engine.rb +7 -0
  10. data/lib/subscribed_to/mail_chimp/config.rb +5 -3
  11. data/lib/subscribed_to/mail_chimp/web_hook.rb +136 -0
  12. data/lib/subscribed_to/mail_chimp.rb +3 -2
  13. data/lib/subscribed_to/version.rb +1 -1
  14. data/lib/subscribed_to.rb +9 -0
  15. data/spec/app/controllers/subscribed_to/mail_chimp_web_hooks_spec.rb +147 -0
  16. data/spec/dummy/Rakefile +7 -0
  17. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  18. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  19. data/spec/dummy/app/models/user.rb +3 -0
  20. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  21. data/spec/dummy/config/application.rb +45 -0
  22. data/spec/dummy/config/boot.rb +10 -0
  23. data/spec/dummy/config/database.yml +22 -0
  24. data/spec/dummy/config/environment.rb +5 -0
  25. data/spec/dummy/config/environments/development.rb +26 -0
  26. data/spec/dummy/config/environments/production.rb +49 -0
  27. data/spec/dummy/config/environments/test.rb +35 -0
  28. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  29. data/spec/dummy/config/initializers/inflections.rb +10 -0
  30. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  31. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  32. data/spec/dummy/config/initializers/session_store.rb +8 -0
  33. data/spec/dummy/config/initializers/subscribed_to.rb +21 -0
  34. data/spec/dummy/config/locales/en.yml +5 -0
  35. data/spec/dummy/config/routes.rb +2 -0
  36. data/spec/dummy/config.ru +4 -0
  37. data/spec/dummy/db/migrate/1_create_users.rb +18 -0
  38. data/spec/dummy/db/schema.rb +26 -0
  39. data/spec/dummy/public/404.html +26 -0
  40. data/spec/dummy/public/422.html +26 -0
  41. data/spec/dummy/public/500.html +26 -0
  42. data/spec/dummy/public/favicon.ico +0 -0
  43. data/spec/dummy/public/javascripts/application.js +2 -0
  44. data/spec/dummy/public/javascripts/controls.js +965 -0
  45. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  46. data/spec/dummy/public/javascripts/effects.js +1123 -0
  47. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  48. data/spec/dummy/public/javascripts/rails.js +191 -0
  49. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  50. data/spec/dummy/script/rails +6 -0
  51. data/spec/factories/users.rb +1 -0
  52. data/spec/{generators → lib/generators}/install_generator_spec.rb +0 -0
  53. data/spec/{subscribed_to → lib/subscribed_to}/mail_chimp/config_spec.rb +0 -0
  54. data/spec/lib/subscribed_to/mail_chimp/web_hook_spec.rb +158 -0
  55. data/spec/lib/subscribed_to/mail_chimp_spec.rb +74 -0
  56. data/spec/{subscribed_to_spec.rb → lib/subscribed_to_spec.rb} +0 -0
  57. data/spec/spec_helper.rb +70 -28
  58. data/subscribed_to.gemspec +136 -0
  59. metadata +77 -26
  60. data/spec/app/models/user.rb +0 -43
  61. data/spec/database.yml.example +0 -17
  62. data/spec/schema.rb +0 -9
  63. data/spec/subscribed_to/mail_chimp_spec.rb +0 -53
@@ -0,0 +1,191 @@
1
+ (function() {
2
+ // Technique from Juriy Zaytsev
3
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
+ function isEventSupported(eventName) {
5
+ var el = document.createElement('div');
6
+ eventName = 'on' + eventName;
7
+ var isSupported = (eventName in el);
8
+ if (!isSupported) {
9
+ el.setAttribute(eventName, 'return;');
10
+ isSupported = typeof el[eventName] == 'function';
11
+ }
12
+ el = null;
13
+ return isSupported;
14
+ }
15
+
16
+ function isForm(element) {
17
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
+ }
19
+
20
+ function isInput(element) {
21
+ if (Object.isElement(element)) {
22
+ var name = element.nodeName.toUpperCase()
23
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
+ }
25
+ else return false
26
+ }
27
+
28
+ var submitBubbles = isEventSupported('submit'),
29
+ changeBubbles = isEventSupported('change')
30
+
31
+ if (!submitBubbles || !changeBubbles) {
32
+ // augment the Event.Handler class to observe custom events when needed
33
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
+ function(init, element, eventName, selector, callback) {
35
+ init(element, eventName, selector, callback)
36
+ // is the handler being attached to an element that doesn't support this event?
37
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
+ // "submit" => "emulated:submit"
40
+ this.eventName = 'emulated:' + this.eventName
41
+ }
42
+ }
43
+ )
44
+ }
45
+
46
+ if (!submitBubbles) {
47
+ // discover forms on the page by observing focus events which always bubble
48
+ document.on('focusin', 'form', function(focusEvent, form) {
49
+ // special handler for the real "submit" event (one-time operation)
50
+ if (!form.retrieve('emulated:submit')) {
51
+ form.on('submit', function(submitEvent) {
52
+ var emulated = form.fire('emulated:submit', submitEvent, true)
53
+ // if custom event received preventDefault, cancel the real one too
54
+ if (emulated.returnValue === false) submitEvent.preventDefault()
55
+ })
56
+ form.store('emulated:submit', true)
57
+ }
58
+ })
59
+ }
60
+
61
+ if (!changeBubbles) {
62
+ // discover form inputs on the page
63
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
+ // special handler for real "change" events
65
+ if (!input.retrieve('emulated:change')) {
66
+ input.on('change', function(changeEvent) {
67
+ input.fire('emulated:change', changeEvent, true)
68
+ })
69
+ input.store('emulated:change', true)
70
+ }
71
+ })
72
+ }
73
+
74
+ function handleRemote(element) {
75
+ var method, url, params;
76
+
77
+ var event = element.fire("ajax:before");
78
+ if (event.stopped) return false;
79
+
80
+ if (element.tagName.toLowerCase() === 'form') {
81
+ method = element.readAttribute('method') || 'post';
82
+ url = element.readAttribute('action');
83
+ params = element.serialize();
84
+ } else {
85
+ method = element.readAttribute('data-method') || 'get';
86
+ url = element.readAttribute('href');
87
+ params = {};
88
+ }
89
+
90
+ new Ajax.Request(url, {
91
+ method: method,
92
+ parameters: params,
93
+ evalScripts: true,
94
+
95
+ onComplete: function(request) { element.fire("ajax:complete", request); },
96
+ onSuccess: function(request) { element.fire("ajax:success", request); },
97
+ onFailure: function(request) { element.fire("ajax:failure", request); }
98
+ });
99
+
100
+ element.fire("ajax:after");
101
+ }
102
+
103
+ function handleMethod(element) {
104
+ var method = element.readAttribute('data-method'),
105
+ url = element.readAttribute('href'),
106
+ csrf_param = $$('meta[name=csrf-param]')[0],
107
+ csrf_token = $$('meta[name=csrf-token]')[0];
108
+
109
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
+ element.parentNode.insert(form);
111
+
112
+ if (method !== 'post') {
113
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
+ form.insert(field);
115
+ }
116
+
117
+ if (csrf_param) {
118
+ var param = csrf_param.readAttribute('content'),
119
+ token = csrf_token.readAttribute('content'),
120
+ field = new Element('input', { type: 'hidden', name: param, value: token });
121
+ form.insert(field);
122
+ }
123
+
124
+ form.submit();
125
+ }
126
+
127
+
128
+ document.on("click", "*[data-confirm]", function(event, element) {
129
+ var message = element.readAttribute('data-confirm');
130
+ if (!confirm(message)) event.stop();
131
+ });
132
+
133
+ document.on("click", "a[data-remote]", function(event, element) {
134
+ if (event.stopped) return;
135
+ handleRemote(element);
136
+ event.stop();
137
+ });
138
+
139
+ document.on("click", "a[data-method]", function(event, element) {
140
+ if (event.stopped) return;
141
+ handleMethod(element);
142
+ event.stop();
143
+ });
144
+
145
+ document.on("submit", function(event) {
146
+ var element = event.findElement(),
147
+ message = element.readAttribute('data-confirm');
148
+ if (message && !confirm(message)) {
149
+ event.stop();
150
+ return false;
151
+ }
152
+
153
+ var inputs = element.select("input[type=submit][data-disable-with]");
154
+ inputs.each(function(input) {
155
+ input.disabled = true;
156
+ input.writeAttribute('data-original-value', input.value);
157
+ input.value = input.readAttribute('data-disable-with');
158
+ });
159
+
160
+ var element = event.findElement("form[data-remote]");
161
+ if (element) {
162
+ handleRemote(element);
163
+ event.stop();
164
+ }
165
+ });
166
+
167
+ document.on("ajax:after", "form", function(event, element) {
168
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
+ inputs.each(function(input) {
170
+ input.value = input.readAttribute('data-original-value');
171
+ input.removeAttribute('data-original-value');
172
+ input.disabled = false;
173
+ });
174
+ });
175
+
176
+ Ajax.Responders.register({
177
+ onCreate: function(request) {
178
+ var csrf_meta_tag = $$('meta[name=csrf-token]')[0];
179
+
180
+ if (csrf_meta_tag) {
181
+ var header = 'X-CSRF-Token',
182
+ token = csrf_meta_tag.readAttribute('content');
183
+
184
+ if (!request.options.requestHeaders) {
185
+ request.options.requestHeaders = {};
186
+ }
187
+ request.options.requestHeaders[header] = token;
188
+ }
189
+ }
190
+ });
191
+ })();
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -3,6 +3,7 @@ Factory.define :subscribed_user, :class => User do |u|
3
3
  u.last_name "Salczynski"
4
4
  u.email "eric@wehaventthetime.com"
5
5
  u.subscribed_to_list true
6
+ u.mail_chimp_id "123"
6
7
  u.password "abc123"
7
8
  end
8
9
 
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe SubscribedTo::MailChimp::WebHook do
4
+ before do
5
+ SubscribedTo.mail_chimp do |config|
6
+ config.lists = {
7
+ :mailing_list => {
8
+ :id => "abc123",
9
+ :merge_vars => {"FNAME" => :first_name, "LNAME" => :last_name, "EMAIL" => :email}}}
10
+
11
+ # normally not set in config, but necessary for testing
12
+ config.enabled_models = {"abc123" => ["User"]}
13
+ end
14
+ end
15
+
16
+ it "should quietly fail and log the error if no member is found" do
17
+ Rails.logger.expects(:warn).once
18
+ expect do
19
+ SubscribedTo::MailChimp::WebHook.process({
20
+ "type" => "subscribe",
21
+ "data" => {
22
+ "list_id" => "abc123",
23
+ "web_id" => "231546749",
24
+ "merges" => {
25
+ "EMAIL" => "fake.user@email.com" }}})
26
+ end.not_to raise_exception
27
+ end
28
+
29
+ it "should rate limit updates via the api to once every 2 minutes" do
30
+ # user was updated 2:01 ago
31
+ pretend_now_is(Time.zone.now - 120.seconds) do
32
+ @user = Factory.build(:non_subscribed_user)
33
+ @user.stubs(:subscribe_to_list)
34
+ @user.save
35
+ end
36
+
37
+ # pretend it's 1:59 from last update
38
+ pretend_now_is(Time.zone.now - 1.seconds) do
39
+ expect do
40
+ SubscribedTo::MailChimp::WebHook.process({
41
+ "type" => "upemail",
42
+ "data" => {
43
+ "list_id" => "abc123",
44
+ "new_email" => "my.new@email.com",
45
+ "old_email" => @user.email }})
46
+ end.not_to change { @user.reload.email }.to("my.new@email.com")
47
+ end
48
+
49
+ # pretend it's 2:01 from last update
50
+ pretend_now_is(Time.zone.now + 120.seconds) do
51
+ expect do
52
+ SubscribedTo::MailChimp::WebHook.process({
53
+ "type" => "upemail",
54
+ "data" => {
55
+ "list_id" => "abc123",
56
+ "new_email" => "my.new@email.com",
57
+ "old_email" => @user.email }})
58
+ end.to change { @user.reload.email }.to("my.new@email.com")
59
+ end
60
+ end
61
+
62
+ it "should write a warning to the logger if the event is not supported" do
63
+ Rails.logger.expects(:warn).once
64
+ expect do
65
+ SubscribedTo::MailChimp::WebHook.process({
66
+ "type" => "nonevent",
67
+ "data" => { "list_id" => "abc123" }})
68
+ end.not_to raise_exception
69
+ end
70
+
71
+ context "for a new user" do
72
+ before(:each) do
73
+ # user was updated 2:01 ago
74
+ pretend_now_is(Time.zone.now - 121.seconds) do
75
+ @user = Factory.build(:non_subscribed_user)
76
+ @user.stubs(:subscribe_to_list)
77
+ @user.save
78
+ @user.expects(:update_list_member).never
79
+ end
80
+ end
81
+
82
+ context "when they subscribe" do
83
+ it "should set subscribed_to_list to true, and set the mail_chimp_id" do
84
+ @user.mail_chimp_id.should be_nil
85
+ expect do
86
+ expect do
87
+ SubscribedTo::MailChimp::WebHook.process({
88
+ "type" => "subscribe",
89
+ "data" => {
90
+ "list_id" => "abc123",
91
+ "web_id" => "231546749",
92
+ "merges" => {
93
+ "EMAIL" => @user.email }}})
94
+ end.to change { @user.reload.subscribed_to_list }.from(false).to(true)
95
+ end.to change { @user.mail_chimp_id }.to(231546749)
96
+ end
97
+ end
98
+ end
99
+
100
+ context "for an existing user" do
101
+ before(:each) do
102
+ # user was updated 2:01 ago
103
+ pretend_now_is(Time.zone.now - 121.seconds) do
104
+ @user = Factory.build(:subscribed_user)
105
+ @user.stubs(:subscribe_to_list)
106
+ @user.save
107
+ @user.expects(:update_list_member).never
108
+ end
109
+ end
110
+
111
+ context "when they unsubscribe" do
112
+ it "should set subscribed_to_list to false" do
113
+ expect do
114
+ SubscribedTo::MailChimp::WebHook.process({
115
+ "type" => "unsubscribe",
116
+ "data" => {
117
+ "list_id" => "abc123",
118
+ "web_id" => "123",
119
+ "merges" => {
120
+ "EMAIL" => @user.email }}})
121
+ end.to change { @user.reload.subscribed_to_list }.from(true).to(false)
122
+ end
123
+ end
124
+
125
+ context "when they change their email" do
126
+ it "should update the user email" do
127
+ expect do
128
+ SubscribedTo::MailChimp::WebHook.process({
129
+ "type" => "upemail",
130
+ "data" => {
131
+ "list_id" => "abc123",
132
+ "new_email" => "my.new@email.com",
133
+ "old_email" => @user.email }})
134
+ end.to change { @user.reload.email }.to("my.new@email.com")
135
+ end
136
+ end
137
+
138
+ context "when they change their profile information" do
139
+ it "should update the attributes defined in the merge vars config" do
140
+ expect do
141
+ expect do
142
+ expect do
143
+ SubscribedTo::MailChimp::WebHook.process({
144
+ "type" => "profile",
145
+ "data" => {
146
+ "list_id" => "abc123",
147
+ "web_id" => "123",
148
+ "merges" => {
149
+ "EMAIL" => "my.new@email.com",
150
+ "FNAME" => "John",
151
+ "LNAME" => "Locke" }}})
152
+ end.to change { @user.reload.email }.to("my.new@email.com")
153
+ end.to change { @user.first_name }.to("John")
154
+ end.to change { @user.last_name }.to("Locke")
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'hominid'
3
+
4
+ describe SubscribedTo::MailChimp do
5
+ before(:each) do
6
+ SubscribedTo.mail_chimp do |config|
7
+ config.api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-us1"
8
+ config.lists = {:mailing_list => {:id => "123456", :merge_vars => {"FNAME" => :first_name, "LNAME" => :last_name, "EMAIL" => :email}}}
9
+ end
10
+
11
+ @h = mock("hominid")
12
+ Hominid::API.stubs(:new).returns(@h)
13
+ end
14
+
15
+ context "for a new user" do
16
+ it "should subscribe the user if subscribed_to_list is true" do
17
+ @h.expects(:list_subscribe).once
18
+ Factory(:subscribed_user)
19
+ end
20
+
21
+ it "should not subscribe the user if subscribed_to_list is false" do
22
+ @h.expects(:list_subscribe).never
23
+ Factory(:non_subscribed_user)
24
+ end
25
+
26
+ it "should rescue and log Hominid::APIErrors" do
27
+ @h.expects(:list_subscribe).raises(Hominid::APIError, mock("FaultException", :faultCode => "xxx", :message => "api error"))
28
+ Rails.logger.expects(:warn).once
29
+ Factory(:subscribed_user)
30
+ end
31
+ end
32
+
33
+ context "for an existing user" do
34
+ context "who is not subscribed to the mailing list" do
35
+ before { @user = Factory(:non_subscribed_user) }
36
+
37
+ it "should subscribe the user" do
38
+ @h.expects(:list_subscribe).once
39
+ @user.update_attributes({:subscribed_to_list => true})
40
+ end
41
+ end
42
+
43
+ context "who is subscribed to the mailing list" do
44
+ before do
45
+ @user = Factory.build(:subscribed_user)
46
+ @user.stubs(:subscribe_to_list)
47
+ @user.save
48
+ end
49
+
50
+ it "should unsubscribe the user" do
51
+ @h.expects(:list_unsubscribe).once
52
+ @user.update_attributes({:subscribed_to_list => false})
53
+ end
54
+
55
+ it "should update list member attributes for the user" do
56
+ @h.expects(:list_update_member).once
57
+ @user.update_attributes({:first_name => "Ed", :last_name => "Salczynski", :email => "ed@whtt.me"})
58
+ end
59
+
60
+ it "should not update list member when attributes not defined in merge vars are changed" do
61
+ @h.expects(:list_subscribe).never
62
+ @h.expects(:list_unsubscribe).never
63
+ @h.expects(:list_update_member).never
64
+ @user.update_attributes({:password => "zyx321"})
65
+ end
66
+
67
+ it "should rescue and log Hominid::APIErrors" do
68
+ @h.expects(:list_unsubscribe).raises(Hominid::APIError, mock("FaultException", :faultCode => "xxx", :message => "api error"))
69
+ Rails.logger.expects(:warn).once
70
+ @user.update_attributes({:subscribed_to_list => false})
71
+ end
72
+ end
73
+ end
74
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,48 +1,90 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
3
4
  require 'simplecov'
4
- SimpleCov.start
5
+ SimpleCov.start do
6
+ add_filter "/spec/"
7
+ end
5
8
 
6
- require 'rspec'
9
+ require 'database_cleaner'
7
10
  require 'factory_girl'
8
- require 'subscribed_to'
9
11
 
10
- # Requires supporting files with custom matchers and macros, etc,
11
- # in ./support/ and its subdirectories.
12
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+ # Configure Rails Envinronment
13
+ ENV["RAILS_ENV"] = "test"
13
14
 
14
- RSpec.configure do |config|
15
- config.mock_with :mocha
16
- end
15
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
17
16
 
18
- ENV['DB'] ||= 'sqlite3'
17
+ ActionMailer::Base.delivery_method = :test
18
+ ActionMailer::Base.perform_deliveries = true
19
+ ActionMailer::Base.default_url_options[:host] = "test.com"
19
20
 
20
- database_yml = File.expand_path('../database.yml', __FILE__)
21
- if File.exists?(database_yml)
22
- active_record_configuration = YAML.load_file(database_yml)[ENV['DB']]
21
+ Rails.backtrace_cleaner.remove_silencers!
23
22
 
24
- ActiveRecord::Base.establish_connection(active_record_configuration)
25
- ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "log", "debug.log"))
23
+ # Run any available migration
24
+ ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
25
+
26
+ # Load support files
27
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
28
+
29
+ Dir["#{File.dirname(__FILE__)}/factories/*.rb"].each {|f| load f}
30
+
31
+ RSpec.configure do |config|
32
+ config.mock_with :mocha
26
33
 
27
- ActiveRecord::Base.silence do
28
- ActiveRecord::Migration.verbose = false
34
+ config.before(:suite) do
35
+ DatabaseCleaner.strategy = :transaction
36
+ DatabaseCleaner.clean_with(:truncation)
37
+ end
29
38
 
30
- load('schema.rb')
31
- Dir["#{File.dirname(__FILE__)}/app/**/*.rb"].each {|f| load f}
32
- Dir["#{File.dirname(__FILE__)}/factories/*.rb"].each {|f| load f}
39
+ config.before(:each) do
40
+ DatabaseCleaner.start
33
41
  end
34
42
 
35
- else
36
- raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
43
+ config.after(:each) do
44
+ DatabaseCleaner.clean
45
+ end
37
46
  end
38
47
 
39
- Rails.logger = Logger.new(File.join(File.dirname(__FILE__), "log", "debug.log"))
40
48
 
41
- def clean_database!
42
- models = [User]
43
- models.each do |model|
44
- ActiveRecord::Base.connection.execute "DELETE FROM #{model.table_name}"
49
+ # Time warp functionality from https://github.com/harvesthq/time-warp
50
+ # Extend the Time class so that we can offset the time that 'now'
51
+ # returns. This should allow us to effectively time warp for functional
52
+ # tests that require limits per hour, what not.
53
+ if !Time.respond_to?(:real_now) # assures there is no infinite looping when aliasing #now
54
+ Time.class_eval do
55
+ class << self
56
+ attr_accessor :testing_offset
57
+
58
+ alias_method :real_now, :now
59
+ def now
60
+ real_now - testing_offset
61
+ end
62
+ alias_method :new, :now
63
+
64
+ end
65
+ end
66
+ end
67
+ Time.testing_offset = 0
68
+
69
+ def pretend_now_is(*args)
70
+ Time.testing_offset = Time.now - time_from(*args)
71
+ if block_given?
72
+ begin
73
+ yield
74
+ ensure
75
+ reset_to_real_time
76
+ end
45
77
  end
46
78
  end
47
79
 
48
- clean_database!
80
+ ##
81
+ # Reset to real time.
82
+ def reset_to_real_time
83
+ Time.testing_offset = 0
84
+ end
85
+
86
+ def time_from(*args)
87
+ return args[0] if 1 == args.size && args[0].is_a?(Time)
88
+ return args[0].to_time if 1 == args.size && args[0].respond_to?(:to_time) # For example, if it's a Date.
89
+ Time.utc(*args)
90
+ end