saucy 0.14.3 → 0.14.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/Gemfile +2 -1
  3. data/Gemfile.lock +60 -58
  4. data/app/controllers/accounts_controller.rb +0 -1
  5. data/app/controllers/admin/trial_extensions_controller.rb +13 -0
  6. data/app/views/admin/accounts/show.html.erb +13 -0
  7. data/config/routes.rb +1 -0
  8. data/features/run_features.feature +3 -2
  9. data/lib/generators/saucy/features/templates/features/manage_billing.feature +12 -7
  10. data/lib/generators/saucy/features/templates/features/manage_plan.feature +12 -6
  11. data/lib/generators/saucy/features/templates/features/new_account.feature +4 -9
  12. data/lib/generators/saucy/features/templates/features/sign_up.feature +1 -1
  13. data/lib/generators/saucy/features/templates/features/sign_up_coupon.feature +1 -2
  14. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +1 -1
  15. data/lib/generators/saucy/features/templates/features/trial_plans.feature +1 -1
  16. data/lib/saucy/account.rb +8 -0
  17. data/lib/saucy/projects_controller.rb +2 -5
  18. data/lib/saucy/subscription.rb +5 -1
  19. data/lib/saucy/version.rb +1 -1
  20. data/saucy.gemspec +2 -2
  21. data/spec/controllers/accounts_controller_spec.rb +9 -9
  22. data/spec/controllers/admin/trial_extensions_controller_spec.rb +67 -0
  23. data/spec/controllers/invitations_controller_spec.rb +13 -7
  24. data/spec/controllers/memberships_controller_spec.rb +10 -7
  25. data/spec/controllers/profiles_controller_spec.rb +8 -2
  26. data/spec/controllers/projects_controller_spec.rb +20 -16
  27. data/spec/environment.rb +3 -3
  28. data/spec/models/account_spec.rb +29 -0
  29. data/spec/models/subscription_spec.rb +33 -11
  30. data/spec/support/clearance.rb +1 -0
  31. data/spec/views/accounts/show.html.erb_spec.rb +41 -0
  32. metadata +59 -14
  33. data/spec/support/clearance_matchers.rb +0 -55
@@ -29,4 +29,4 @@ Feature: Sign up
29
29
  And I fill in "Email" with "email@person.com"
30
30
  And I fill in "Password" with "password"
31
31
  And I press "Sign up"
32
- Then I should see "created"
32
+ Then I should be on the new project page for the newest account by "email@person.com"
@@ -18,7 +18,7 @@ Feature: Sign up with coupon
18
18
  When I fill in "Email" with "email@person.com"
19
19
  And I fill in "Password" with "password"
20
20
  And I press "Sign up"
21
- Then I should see "created"
21
+ Then I should be on the new project page for the newest account by "email@person.com"
22
22
 
23
23
  Scenario: Receive a reminder about an expiring trial plan with coupon
24
24
  Given a "Free" account with "RAMEN" coupon exists with a name of "Test" created 83 days ago
@@ -28,4 +28,3 @@ Feature: Sign up with coupon
28
28
  And I sign in as "admin@example.com"
29
29
  And I follow the link sent to "admin@example.com" with subject "Your trial is expiring soon"
30
30
  Then I should be on the upgrade plan page for the "Test" account
31
-
@@ -38,7 +38,7 @@ Feature: Sign up
38
38
  And I fill in "ZIP or Postal Code" with "02110"
39
39
  And I select "United States of America" from "Country"
40
40
  And I press "Sign up"
41
- Then I should see "created"
41
+ Then I should be on the new project page for the newest account by "email@person.com"
42
42
 
43
43
  Scenario: User signs up for a paid plan with an invalid credit card number
44
44
  Given that the credit card "4111112" is invalid
@@ -20,7 +20,7 @@ Feature: Trial plans
20
20
  When I fill in "Email" with "email@person.com"
21
21
  And I fill in "Password" with "password"
22
22
  And I press "Sign up"
23
- Then I should see "created"
23
+ Then I should be on the new project page for the newest account by "email@person.com"
24
24
 
25
25
  Scenario: use an account during the trial
26
26
  Given a "Temp" account exists with a name of "Test" created 29 days ago
@@ -83,6 +83,14 @@ module Saucy
83
83
  self.trial_expires_at = number_free_days.days.from_now(created_at || Time.zone.now)
84
84
  end
85
85
 
86
+ def reset_trial_expiration(length_in_days = nil)
87
+ new_length = length_in_days || Saucy::Configuration.trial_length
88
+ new_trial_start_date = [trial_expires_at, Time.zone.now].max
89
+ self.trial_expires_at = new_length.days.from_now(new_trial_start_date)
90
+ self.notified_of_completed_trial = false
91
+ self.notified_of_expiration = false
92
+ end
93
+
86
94
  def record_new_activations
87
95
  if activated_changed? && activated?
88
96
  Saucy::Notifications.notify_observers("activated", :account => self)
@@ -24,7 +24,6 @@ module Saucy
24
24
  def create
25
25
  @project = current_account.projects.build(params[:project])
26
26
  if @project.save
27
- flash[:notice] = "Project successfully created"
28
27
  redirect_to project_url(@project)
29
28
  else
30
29
  render :action => :new
@@ -40,8 +39,7 @@ module Saucy
40
39
  @project = current_project
41
40
  set_project_account_if_moving
42
41
  if @project.update_attributes params[:project]
43
- flash[:success] = 'Project was updated.'
44
- redirect_to account_projects_url(@project.account)
42
+ redirect_to account_projects_url(@project.account), :notice => 'Project was updated.'
45
43
  else
46
44
  render :action => :edit
47
45
  end
@@ -53,8 +51,7 @@ module Saucy
53
51
 
54
52
  def destroy
55
53
  current_project.destroy
56
- flash[:success] = "Project has been deleted"
57
- redirect_to account_projects_url(current_project.account)
54
+ redirect_to account_projects_url(current_project.account), :notice => "Project has been deleted"
58
55
  end
59
56
 
60
57
  def index
@@ -223,7 +223,7 @@ module Saucy
223
223
  Airbrake.notify(nil,
224
224
  :error_class => "handle_errors",
225
225
  :error_message => "handle_errors",
226
- :parameters => { :result => result, :remote_errors => remote_errors }
226
+ :parameters => { :result => result.inspect, :remote_errors => remote_errors.inspect }
227
227
  )
228
228
  if result && result.status == "processor_declined"
229
229
  errors[:card_number] << "was denied by the payment processor with the message: #{result.processor_response_text}"
@@ -239,8 +239,12 @@ module Saucy
239
239
  errors[:expiration_month] << error.message.gsub("Expiration month ", "")
240
240
  elsif error.attribute == "expiration_year"
241
241
  errors[:expiration_year] << error.message.gsub("Expiration year ", "")
242
+ else
243
+ errors[:card_number] << error.message
242
244
  end
243
245
  end
246
+ else
247
+ errors[:card_number] << "there was an unknown status"
244
248
  end
245
249
  end
246
250
 
@@ -1,3 +1,3 @@
1
1
  module Saucy
2
- VERSION = '0.14.3'
2
+ VERSION = '0.14.5'
3
3
  end
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.email = %q{support@thoughtbot.com}
19
19
  s.homepage = "http://github.com/thoughtbot/saucy"
20
20
 
21
- s.add_dependency('clearance', '~> 0.11.2')
22
- s.add_dependency('formtastic', '>= 1.2')
21
+ s.add_dependency('clearance', '~> 0.14.0')
22
+ s.add_dependency('formtastic', '~> 1.2.4')
23
23
  s.add_dependency('railties', '>= 3.0.3')
24
24
  s.add_dependency('braintree', '>= 2.6.2')
25
25
  s.add_dependency('sham_rack', '1.3.3')
@@ -66,8 +66,6 @@ describe AccountsController, "successful create for a confirmed user" do
66
66
  signup.should have_received(:user=).with(user)
67
67
  end
68
68
 
69
- it { should set_the_flash.to(/created/i) }
70
- it { should_not set_the_flash.to(/confirm/i) }
71
69
  it { should be_signed_in.as(user) }
72
70
 
73
71
  it "notifies observers" do
@@ -223,11 +221,13 @@ describe AccountsController, "destroy", :as => :account_admin do
223
221
  end
224
222
 
225
223
  describe AccountsController, "permissions", :as => :account_member do
226
- it { should deny_access.
227
- on(:get, :edit, :id => account.to_param).
228
- flash(/admin/) }
229
- it { should deny_access.
230
- on(:put, :update, :id => account.to_param).
231
- flash(/admin/) }
232
- end
224
+ context "get to account edit" do
225
+ before { get :edit, :id => account.to_param }
226
+ it { should deny_access :flash => "You must be an admin to access that page." }
227
+ end
233
228
 
229
+ context "put to account update" do
230
+ before { put :update, :id => account.to_param }
231
+ it { should deny_access :flash => "You must be an admin to access that page." }
232
+ end
233
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe Admin::TrialExtensionsController, "routes" do
4
+ it { should route(:post, "/admin/accounts/test/trial_extensions").to(:action => :create, :account_id => 'test') }
5
+ end
6
+
7
+ describe Admin::TrialExtensionsController, "post create without http auth" do
8
+ before { post :create, :account_id => "test" }
9
+ it { should respond_with(:unauthorized) }
10
+ end
11
+
12
+ describe Admin::TrialExtensionsController, "post create with http auth" do
13
+ let(:account) { Factory(:account, :keyword => 'test') }
14
+ before do
15
+ http_basic_auth_sign_in 'admin', 'admin'
16
+ post :create, :account_id => account.keyword
17
+ end
18
+ it { should redirect_to(admin_account_path(account.keyword)) }
19
+ end
20
+
21
+ describe Admin::TrialExtensionsController, "extending an unexpired trial" do
22
+ let(:plan) { Factory(:plan, :trial => true) }
23
+ let(:account) { Factory(:account, :keyword => 'test', :plan => plan) }
24
+ let!(:original_trial_expiration_date) { account.trial_expires_at }
25
+ before do
26
+ http_basic_auth_sign_in 'admin', 'admin'
27
+ post :create, :account_id => account.keyword
28
+ account.reload
29
+ end
30
+ it "extends the trial by the standard saucy trial length" do
31
+ difference_in_dates = (account.trial_expires_at - original_trial_expiration_date)
32
+ difference_in_dates.should == Saucy::Configuration.trial_length.days
33
+ end
34
+ end
35
+
36
+ describe Admin::TrialExtensionsController, "extending an expired trial" do
37
+ let(:plan) { Factory(:plan, :trial => true) }
38
+ let(:account) { Factory(:account, :keyword => 'test', :plan => plan) }
39
+ before do
40
+ Timecop.freeze(1.day.ago.beginning_of_day) do
41
+ account.update_attribute(:trial_expires_at, 60.days.ago)
42
+ http_basic_auth_sign_in 'admin', 'admin'
43
+ post :create, :account_id => account.keyword
44
+ account.reload
45
+ end
46
+ end
47
+ it "extends the trial out from today by the standard saucy trial length" do
48
+ Timecop.freeze(1.day.ago.beginning_of_day) do
49
+ difference_in_dates = (account.trial_expires_at - Time.zone.now)
50
+ difference_in_dates.should == Saucy::Configuration.trial_length.days
51
+ end
52
+ end
53
+ end
54
+
55
+ describe Admin::TrialExtensionsController, "extending an invalid account" do
56
+ let(:plan) { Factory(:plan, :trial => true) }
57
+ let(:account) { Factory(:account, :keyword => 'test', :plan => plan) }
58
+ before do
59
+ account.update_attribute(:keyword, '!@#$%')
60
+
61
+ http_basic_auth_sign_in 'admin', 'admin'
62
+ post :create, :account_id => account.keyword
63
+ account.reload
64
+ end
65
+ it { should render_template('admin/accounts/show') }
66
+ it { should set_the_flash.to(/cannot/i) }
67
+ end
@@ -13,13 +13,19 @@ end
13
13
 
14
14
  describe InvitationsController, "permissions" do
15
15
  let(:account) { Factory(:account) }
16
- before { sign_in }
17
- it { should deny_access.
18
- on(:get, :new, :account_id => account.to_param).
19
- flash(/admin/) }
20
- it { should deny_access.
21
- on(:post, :create, :account_id => account.to_param).
22
- flash(/admin/) }
16
+ before do
17
+ sign_in
18
+ end
19
+
20
+ context "on get to new" do
21
+ before { get :new, :account_id => account.to_param }
22
+ it { should deny_access :flash => "You must be an admin to access that page." }
23
+ end
24
+
25
+ context "on post to create" do
26
+ before { post :create, :account_id => account.to_param }
27
+ it { should deny_access :flash => "You must be an admin to access that page." }
28
+ end
23
29
  end
24
30
 
25
31
  describe InvitationsController, "new", :as => :account_admin do
@@ -13,13 +13,16 @@ end
13
13
 
14
14
  describe MembershipsController, "permissions", :as => :account_member do
15
15
  let(:membership) { Factory(:membership, :account => account) }
16
- it { should deny_access.
17
- on(:get, :index, :account_id => account.to_param).
18
- flash(/admin/) }
19
- it { should deny_access.
20
- on(:get, :edit, :id => membership.to_param,
21
- :account_id => account.to_param).
22
- flash(/admin/) }
16
+
17
+ context "on get to index" do
18
+ before { get :index, :account_id => account.to_param }
19
+ it { should deny_access :flash => "You must be an admin to access that page." }
20
+ end
21
+
22
+ context "on get to edit" do
23
+ before { get :edit, :id => membership.to_param, :account_id => account.to_param }
24
+ it { should deny_access :flash => "You must be an admin to access that page." }
25
+ end
23
26
  end
24
27
 
25
28
  describe MembershipsController, "index", :as => :account_admin do
@@ -6,8 +6,14 @@ describe ProfilesController, "routes" do
6
6
  end
7
7
 
8
8
  describe ProfilesController, "signed out" do
9
- it { should deny_access.on(:get, :edit) }
10
- it { should deny_access.on(:put, :update) }
9
+ context "on get to edit" do
10
+ before { get :edit }
11
+ it { should deny_access }
12
+ end
13
+ context "on put to update" do
14
+ before { put :update }
15
+ it { should deny_access }
16
+ end
11
17
  end
12
18
 
13
19
  describe ProfilesController, "edit", :as => :user do
@@ -183,22 +183,26 @@ describe ProjectsController, "index", :as => :account_admin do
183
183
  end
184
184
 
185
185
  describe ProjectsController, "as a non-admin", :as => :project_member do
186
- it { should deny_access.on(:get, :edit, :id => project.to_param,
187
- :account_id => account.to_param).
188
- flash(/admin/) }
189
-
190
- it { should deny_access.on(:put, :update, :id => project.to_param,
191
- :account_id => account.to_param).
192
- flash(/admin/) }
193
-
194
- it { should deny_access.on(:delete, :destroy, :id => project.to_param,
195
- :account_id => account.to_param).
196
- flash(/admin/) }
197
-
198
- it { should deny_access.on(:get, :new, :account_id => account.to_param).flash(/admin/) }
199
-
200
- it { should deny_access.on(:post, :create, :account_id => account.to_param).
201
- flash(/admin/) }
186
+ context "on get to edit" do
187
+ before { get :edit, :id => project.to_param, :account_id => account.to_param }
188
+ it { should deny_access :flash => "You must be an admin to access that page." }
189
+ end
190
+ context "on put to update" do
191
+ before { put :update, :id => project.to_param, :account_id => account.to_param }
192
+ it { should deny_access :flash => "You must be an admin to access that page." }
193
+ end
194
+ context "on delete to destroy" do
195
+ before { delete :destroy, :id => project.to_param, :account_id => account.to_param }
196
+ it { should deny_access :flash => "You must be an admin to access that page." }
197
+ end
198
+ context "on get to new" do
199
+ before { get :new, :account_id => account.to_param }
200
+ it { should deny_access :flash => "You must be an admin to access that page." }
201
+ end
202
+ context "on post to create" do
203
+ before { post :create, :account_id => account.to_param }
204
+ it { should deny_access :flash => "You must be an admin to access that page." }
205
+ end
202
206
  end
203
207
 
204
208
  describe ProjectsController, "show for a duplicate project keyword", :as => :project_admin do
@@ -9,6 +9,9 @@ require 'bourne'
9
9
 
10
10
  FileUtils.rm_f(File.join(PROJECT_ROOT, 'tmp', 'test.sqlite3'))
11
11
 
12
+ Clearance.configure do |config|
13
+ end
14
+
12
15
  class ApplicationController < ActionController::Base
13
16
  include Clearance::Authentication
14
17
  include Saucy::AccountAuthorization
@@ -90,9 +93,6 @@ class ClearanceMailer
90
93
  end
91
94
  end
92
95
 
93
- Clearance.configure do |config|
94
- end
95
-
96
96
  ClearanceCreateUsers.suppress_messages { ClearanceCreateUsers.migrate(:up) }
97
97
  CreateSaucyTables.suppress_messages { CreateSaucyTables.migrate(:up) }
98
98
  ActiveRecord::Migrator.up(ActiveRecord::Migrator.migrations_paths)
@@ -133,6 +133,35 @@ describe Account do
133
133
  account.should_not be_expired
134
134
  end
135
135
 
136
+ it "extends the trial period on unexpired accounts" do
137
+ Timecop.freeze do
138
+ plan = Factory.stub(:plan, :trial => true)
139
+ account = Factory(:account, :plan => plan)
140
+ original_expiration = account.trial_expires_at
141
+
142
+ account.reset_trial_expiration
143
+
144
+ (account.trial_expires_at - original_expiration).should == Saucy::Configuration.trial_length.days
145
+ end
146
+ end
147
+
148
+ it "resets the trial period and emails on expired accounts" do
149
+ Timecop.freeze do
150
+ plan = Factory.stub(:plan, :trial => true)
151
+ account = Factory(:account,
152
+ :plan => plan,
153
+ :notified_of_completed_trial => true,
154
+ :notified_of_expiration => true,
155
+ :created_at => 1.year.ago)
156
+
157
+ account.reset_trial_expiration
158
+
159
+ (account.trial_expires_at - Time.zone.now).should == Saucy::Configuration.trial_length.days
160
+ account.notified_of_expiration.should be_false
161
+ account.notified_of_completed_trial.should be_false
162
+ end
163
+ end
164
+
136
165
  context "with expiring accounts" do
137
166
  before do
138
167
  trial = Factory(:plan, :trial => true)
@@ -16,7 +16,7 @@ describe Account do
16
16
  it "manifests braintree processor_declined errors as errors on number and doesn't save" do
17
17
  FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" }
18
18
  account = Factory.build(:paid_account,
19
- :card_number => "4111111111111112",
19
+ :card_number => "4111111111111112",
20
20
  :plan => Factory(:paid_plan))
21
21
  account.save.should_not be
22
22
  FakeBraintree.customers.should be_empty
@@ -27,7 +27,7 @@ describe Account do
27
27
  it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
28
28
  FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" }
29
29
  account = Factory.build(:paid_account,
30
- :card_number => "4111111111111112",
30
+ :card_number => "4111111111111112",
31
31
  :plan => Factory(:paid_plan))
32
32
  account.save.should_not be
33
33
  FakeBraintree.customers.should be_empty
@@ -38,13 +38,35 @@ describe Account do
38
38
  it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do
39
39
  FakeBraintree.failures["4111111111111112"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}}
40
40
  account = Factory.build(:paid_account,
41
- :card_number => "4111111111111112",
41
+ :card_number => "4111111111111112",
42
42
  :plan => Factory(:paid_plan))
43
43
  account.save.should_not be
44
44
  FakeBraintree.customers.should be_empty
45
45
  account.persisted?.should_not be
46
46
  account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be
47
- end
47
+ end
48
+
49
+ it "manifests braintree gateway_rejected unknown errors and doesn't save" do
50
+ FakeBraintree.failures["4111111111111112"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "There was an unkown error.", "code" => 81715, "attribute" => :unknown }] }}}}
51
+ account = Factory.build(:paid_account,
52
+ :card_number => "4111111111111112",
53
+ :plan => Factory(:paid_plan))
54
+ account.save.should_not be
55
+ FakeBraintree.customers.should be_empty
56
+ account.persisted?.should_not be
57
+ account.errors[:card_number].any? { |e| e =~ /There was an unkown error./ }.should be
58
+ end
59
+
60
+ it "manifests braintree unknown status error" do
61
+ FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "unknown" }
62
+ account = Factory.build(:paid_account,
63
+ :card_number => "4111111111111112",
64
+ :plan => Factory(:paid_plan))
65
+ account.save.should_not be
66
+ FakeBraintree.customers.should be_empty
67
+ account.persisted?.should_not be
68
+ account.errors[:card_number].any? { |e| e =~ /unknown status/ }.should be
69
+ end
48
70
  end
49
71
 
50
72
  describe Account, "given free and paid plans" do
@@ -139,10 +161,10 @@ describe Account, "with a paid plan" do
139
161
  context "updating customer credit card information when changed" do
140
162
  before do
141
163
  subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com",
142
- :cardholder_name => "Jim Robot",
164
+ :cardholder_name => "Jim Robot",
143
165
  :card_number => "4111111111111115",
144
166
  :verification_code => "123",
145
- :expiration_month => 5,
167
+ :expiration_month => 5,
146
168
  :expiration_year => 2013)
147
169
  end
148
170
 
@@ -170,10 +192,10 @@ describe Account, "with a paid plan" do
170
192
  context "updating customer credit card billing address information when changed" do
171
193
  before do
172
194
  subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com",
173
- :cardholder_name => "Jim Robot",
195
+ :cardholder_name => "Jim Robot",
174
196
  :card_number => "4111111111111115",
175
197
  :verification_code => "123",
176
- :expiration_month => 5,
198
+ :expiration_month => 5,
177
199
  :expiration_year => 2013,
178
200
  :street_address => "1 E Main St",
179
201
  :extended_address => "Suite 3",
@@ -322,7 +344,7 @@ end
322
344
 
323
345
  describe Account, "with a paid subscription" do
324
346
  subject do
325
- Factory(:paid_account,
347
+ Factory(:paid_account,
326
348
  :plan => Factory(:paid_plan))
327
349
  end
328
350
 
@@ -358,7 +380,7 @@ describe Account, "with a paid subscription" do
358
380
  end
359
381
 
360
382
  it "delivers the rest of the emails even if one fails" do
361
- Factory(:paid_account,
383
+ Factory(:paid_account,
362
384
  :plan => Factory(:paid_plan))
363
385
  Timecop.travel(subject.next_billing_date + 1.day) do
364
386
  Account.update_subscriptions!
@@ -545,7 +567,7 @@ end
545
567
 
546
568
  describe Account, "with a paid subscription that is past due" do
547
569
  subject do
548
- Factory(:paid_account,
570
+ Factory(:paid_account,
549
571
  :plan => Factory(:paid_plan))
550
572
  end
551
573