saasy 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.
- data/CHANGELOG.md +114 -0
- data/Gemfile +26 -0
- data/README.md +118 -0
- data/Rakefile +38 -0
- data/app/controllers/accounts_controller.rb +68 -0
- data/app/controllers/billings_controller.rb +25 -0
- data/app/controllers/invitations_controller.rb +65 -0
- data/app/controllers/memberships_controller.rb +45 -0
- data/app/controllers/plans_controller.rb +24 -0
- data/app/controllers/profiles_controller.rb +19 -0
- data/app/helpers/limits_helper.rb +13 -0
- data/app/mailers/billing_mailer.rb +53 -0
- data/app/mailers/invitation_mailer.rb +18 -0
- data/app/models/invitation.rb +113 -0
- data/app/models/limit.rb +49 -0
- data/app/models/membership.rb +26 -0
- data/app/models/permission.rb +19 -0
- data/app/models/signup.rb +163 -0
- data/app/views/accounts/_account.html.erb +9 -0
- data/app/views/accounts/_blank_slate.html.erb +6 -0
- data/app/views/accounts/_projects.html.erb +12 -0
- data/app/views/accounts/_subnav.html.erb +10 -0
- data/app/views/accounts/edit.html.erb +34 -0
- data/app/views/accounts/index.html.erb +9 -0
- data/app/views/accounts/new.html.erb +36 -0
- data/app/views/billing_mailer/completed_trial.text.erb +13 -0
- data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
- data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
- data/app/views/billing_mailer/problem.html.erb +13 -0
- data/app/views/billing_mailer/problem.text.erb +14 -0
- data/app/views/billing_mailer/receipt.html.erb +41 -0
- data/app/views/billing_mailer/receipt.text.erb +25 -0
- data/app/views/billings/_form.html.erb +8 -0
- data/app/views/billings/edit.html.erb +13 -0
- data/app/views/billings/show.html.erb +29 -0
- data/app/views/invitation_mailer/invitation.text.erb +6 -0
- data/app/views/invitations/new.html.erb +17 -0
- data/app/views/invitations/show.html.erb +22 -0
- data/app/views/layouts/saucy.html.erb +36 -0
- data/app/views/limits/_meter.html.erb +13 -0
- data/app/views/memberships/edit.html.erb +21 -0
- data/app/views/memberships/index.html.erb +17 -0
- data/app/views/plans/_plan.html.erb +32 -0
- data/app/views/plans/_terms.html.erb +15 -0
- data/app/views/plans/edit.html.erb +33 -0
- data/app/views/plans/index.html.erb +12 -0
- data/app/views/profiles/_inputs.html.erb +5 -0
- data/app/views/profiles/edit.html.erb +36 -0
- data/app/views/projects/_form.html.erb +36 -0
- data/app/views/projects/edit.html.erb +22 -0
- data/app/views/projects/index.html.erb +28 -0
- data/app/views/projects/new.html.erb +13 -0
- data/app/views/projects/show.html.erb +0 -0
- data/app/views/shared/_project_dropdown.html.erb +55 -0
- data/app/views/shared/_saucy_javascript.html.erb +33 -0
- data/config/locales/en.yml +37 -0
- data/config/routes.rb +19 -0
- data/features/run_features.feature +83 -0
- data/features/step_definitions/clearance_steps.rb +45 -0
- data/features/step_definitions/rails_steps.rb +73 -0
- data/features/step_definitions/saucy_steps.rb +8 -0
- data/features/support/env.rb +4 -0
- data/features/support/file.rb +11 -0
- data/lib/generators/saucy/base.rb +18 -0
- data/lib/generators/saucy/features/features_generator.rb +91 -0
- data/lib/generators/saucy/features/templates/README +3 -0
- data/lib/generators/saucy/features/templates/factories.rb +71 -0
- data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
- data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
- data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
- data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
- data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
- data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
- data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
- data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
- data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
- data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
- data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
- data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
- data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
- data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
- data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
- data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
- data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
- data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
- data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
- data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
- data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
- data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
- data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
- data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/install/install_generator.rb +40 -0
- data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
- data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
- data/lib/generators/saucy/install/templates/models/account.rb +3 -0
- data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
- data/lib/generators/saucy/install/templates/models/project.rb +3 -0
- data/lib/generators/saucy/specs/specs_generator.rb +20 -0
- data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/views/views_generator.rb +23 -0
- data/lib/saucy.rb +10 -0
- data/lib/saucy/account.rb +132 -0
- data/lib/saucy/account_authorization.rb +67 -0
- data/lib/saucy/configuration.rb +29 -0
- data/lib/saucy/engine.rb +35 -0
- data/lib/saucy/fake_braintree.rb +134 -0
- data/lib/saucy/layouts.rb +36 -0
- data/lib/saucy/plan.rb +54 -0
- data/lib/saucy/project.rb +125 -0
- data/lib/saucy/projects_controller.rb +94 -0
- data/lib/saucy/railties/tasks.rake +28 -0
- data/lib/saucy/routing_extensions.rb +121 -0
- data/lib/saucy/subscription.rb +237 -0
- data/lib/saucy/user.rb +30 -0
- data/spec/controllers/accounts_controller_spec.rb +228 -0
- data/spec/controllers/application_controller_spec.rb +32 -0
- data/spec/controllers/invitations_controller_spec.rb +215 -0
- data/spec/controllers/memberships_controller_spec.rb +117 -0
- data/spec/controllers/plans_controller_spec.rb +13 -0
- data/spec/controllers/profiles_controller_spec.rb +48 -0
- data/spec/controllers/projects_controller_spec.rb +216 -0
- data/spec/environment.rb +95 -0
- data/spec/layouts_spec.rb +21 -0
- data/spec/mailers/billing_mailer_spec.rb +68 -0
- data/spec/mailers/invitiation_mailer_spec.rb +19 -0
- data/spec/models/account_spec.rb +218 -0
- data/spec/models/invitation_spec.rb +320 -0
- data/spec/models/limit_spec.rb +70 -0
- data/spec/models/membership_spec.rb +37 -0
- data/spec/models/permission_spec.rb +30 -0
- data/spec/models/plan_spec.rb +81 -0
- data/spec/models/project_spec.rb +223 -0
- data/spec/models/signup_spec.rb +177 -0
- data/spec/models/subscription_spec.rb +481 -0
- data/spec/models/user_spec.rb +72 -0
- data/spec/route_extensions_spec.rb +51 -0
- data/spec/saucy_spec.rb +62 -0
- data/spec/scaffold/config/routes.rb +5 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/authentication_helpers.rb +81 -0
- data/spec/support/authorization_helpers.rb +56 -0
- data/spec/support/braintree.rb +7 -0
- data/spec/support/clearance_matchers.rb +55 -0
- data/spec/support/notifications.rb +57 -0
- data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
- metadata +325 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Saucy::Layouts do
|
|
4
|
+
it "allows a layout to be assigned" do
|
|
5
|
+
subject.accounts.index = "custom"
|
|
6
|
+
subject.accounts.index.should == "custom"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "defaults to the saucy layout" do
|
|
10
|
+
subject.accounts.index.should == "saucy"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "selects a layout for a controller" do
|
|
14
|
+
controller = AccountsController.new
|
|
15
|
+
controller.stubs(:action_name => 'index')
|
|
16
|
+
block = subject.class.to_proc
|
|
17
|
+
|
|
18
|
+
block.call(controller).should == 'saucy'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "with an account and transaction" do
|
|
4
|
+
let(:account) { Factory(:paid_account) }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
subscription = FakeBraintree.subscriptions[account.subscription_token]
|
|
8
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe BillingMailer, "receipt" do
|
|
12
|
+
subject { BillingMailer.receipt(account, account.subscription.transactions.first) }
|
|
13
|
+
|
|
14
|
+
it "sends the receipt mail from the support address" do
|
|
15
|
+
subject.from.should == [Saucy::Configuration.support_email_address]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "sets the receipt mail reply-to to the support address" do
|
|
19
|
+
subject.reply_to.should == [Saucy::Configuration.support_email_address]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe BillingMailer, "problem" do
|
|
24
|
+
subject { BillingMailer.problem(account, account.subscription.transactions.first) }
|
|
25
|
+
|
|
26
|
+
it "sends the problem mail from the support address" do
|
|
27
|
+
subject.from.should == [Saucy::Configuration.support_email_address]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "sets the problem mail reply-to to the support address" do
|
|
31
|
+
subject.reply_to.should == [Saucy::Configuration.support_email_address]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe BillingMailer, "expiring trial" do
|
|
36
|
+
subject { BillingMailer.expiring_trial(account) }
|
|
37
|
+
|
|
38
|
+
it "sends the expiring trial mail from the support address" do
|
|
39
|
+
subject.from.should == [Saucy::Configuration.manager_email_address]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "sets the expiring trial mail reply-to to the support address" do
|
|
43
|
+
subject.reply_to.should == [Saucy::Configuration.support_email_address]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe BillingMailer, "new unactivated" do
|
|
48
|
+
subject { BillingMailer.expiring_trial(account) }
|
|
49
|
+
|
|
50
|
+
it "sends the new unactivated mail from the support address" do
|
|
51
|
+
subject.from.should == [Saucy::Configuration.manager_email_address]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "sets the new unactivated mail reply-to to the support address" do
|
|
55
|
+
subject.reply_to.should == [Saucy::Configuration.support_email_address]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe BillingMailer, "completed trial" do
|
|
60
|
+
subject { BillingMailer.completed_trial(account) }
|
|
61
|
+
|
|
62
|
+
it "uses the completed trial notice" do
|
|
63
|
+
notice = I18n.translate!("billing_mailer.completed_trial.notice",
|
|
64
|
+
:account_name => account.name)
|
|
65
|
+
subject.body.should include(notice)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe InvitationMailer, "invitation" do
|
|
4
|
+
let(:invitation) { Factory(:invitation) }
|
|
5
|
+
subject { InvitationMailer.invitation(invitation) }
|
|
6
|
+
|
|
7
|
+
it "sets reply-to to the user that sent the invitation" do
|
|
8
|
+
subject.reply_to.should == [invitation.sender_email]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "sets from to the user's name" do
|
|
12
|
+
from = subject.header_fields.detect { |field| field.name == "From"}
|
|
13
|
+
from.value.should =~ %r{^"#{invitation.sender_name}" <.*>$}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "sends from the support address" do
|
|
17
|
+
subject.from.should == [Saucy::Configuration.support_email_address]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Account do
|
|
4
|
+
subject { Factory(:account) }
|
|
5
|
+
|
|
6
|
+
it { should have_many(:memberships) }
|
|
7
|
+
it { should have_many(:users).through(:memberships) }
|
|
8
|
+
it { should have_many(:projects) }
|
|
9
|
+
it { should belong_to(:plan) }
|
|
10
|
+
|
|
11
|
+
it { should validate_uniqueness_of(:keyword) }
|
|
12
|
+
it { should validate_presence_of( :name) }
|
|
13
|
+
it { should validate_presence_of(:keyword) }
|
|
14
|
+
it { should validate_presence_of(:plan_id) }
|
|
15
|
+
|
|
16
|
+
it { should allow_value("hello").for(:keyword) }
|
|
17
|
+
it { should allow_value("0123").for(:keyword) }
|
|
18
|
+
it { should allow_value("hello_world").for(:keyword) }
|
|
19
|
+
it { should allow_value("hello-world").for(:keyword) }
|
|
20
|
+
it { should_not allow_value("HELLO").for(:keyword) }
|
|
21
|
+
it { should_not allow_value("hello world").for(:keyword) }
|
|
22
|
+
|
|
23
|
+
it { should_not allow_mass_assignment_of(:id) }
|
|
24
|
+
it { should_not allow_mass_assignment_of(:updated_at) }
|
|
25
|
+
it { should_not allow_mass_assignment_of(:created_at) }
|
|
26
|
+
it { should allow_mass_assignment_of(:keyword) }
|
|
27
|
+
|
|
28
|
+
[nil, "", "a b", "a.b", "a%b"].each do |value|
|
|
29
|
+
it { should_not allow_value(value).for(:keyword).with_message(/letters/i) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
["foo", "f00", "37signals"].each do |value|
|
|
33
|
+
it { should allow_value(value).for(:keyword) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "should give its keyword for to_param" do
|
|
37
|
+
subject.to_param.should == subject.keyword
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "finds admin users" do
|
|
41
|
+
admins = [Factory(:user), Factory(:user)]
|
|
42
|
+
non_admin = Factory(:user)
|
|
43
|
+
non_member = Factory(:user)
|
|
44
|
+
admins.each do |admin|
|
|
45
|
+
Factory(:membership, :user => admin, :account => subject, :admin => true)
|
|
46
|
+
end
|
|
47
|
+
Factory(:membership, :user => non_admin, :account => subject, :admin => false)
|
|
48
|
+
|
|
49
|
+
result = subject.admins
|
|
50
|
+
|
|
51
|
+
result.to_a.should =~ admins
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "finds non admin users" do
|
|
55
|
+
non_admins = [Factory(:user), Factory(:user)]
|
|
56
|
+
admin = Factory(:user)
|
|
57
|
+
non_member = Factory(:user)
|
|
58
|
+
non_admins.each do |non_admin|
|
|
59
|
+
Factory(:membership, :user => non_admin, :account => subject, :admin => false)
|
|
60
|
+
end
|
|
61
|
+
Factory(:membership, :user => admin, :account => subject, :admin => true)
|
|
62
|
+
|
|
63
|
+
result = subject.non_admins
|
|
64
|
+
|
|
65
|
+
result.to_a.should =~ non_admins
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "finds emails for admin users" do
|
|
69
|
+
admins = [Factory(:user), Factory(:user)]
|
|
70
|
+
non_admin = Factory(:user)
|
|
71
|
+
non_member = Factory(:user)
|
|
72
|
+
admins.each do |admin|
|
|
73
|
+
Factory(:membership, :user => admin, :account => subject, :admin => true)
|
|
74
|
+
end
|
|
75
|
+
Factory(:membership, :user => non_admin, :account => subject, :admin => false)
|
|
76
|
+
|
|
77
|
+
subject.admin_emails.should == admins.map(&:email)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "has a member with a membership" do
|
|
81
|
+
membership = Factory(:membership, :account => subject)
|
|
82
|
+
should have_member(membership.user)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "has a count of users" do
|
|
86
|
+
membership = Factory(:membership, :account => subject)
|
|
87
|
+
subject.users_count.should == 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "has a count of active projects" do
|
|
91
|
+
Factory(:project, :account => subject, :archived => false)
|
|
92
|
+
Factory(:project, :account => subject, :archived => true)
|
|
93
|
+
subject.projects_count.should == 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "doesn't have a member without a membership" do
|
|
97
|
+
membership = Factory(:membership, :account => subject)
|
|
98
|
+
should_not have_member(Factory(:user))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "finds memberships by name" do
|
|
102
|
+
expected = 'expected result'
|
|
103
|
+
memberships = stub('memberships', :by_name => expected)
|
|
104
|
+
account = Factory.stub(:account)
|
|
105
|
+
account.stubs(:memberships => memberships)
|
|
106
|
+
|
|
107
|
+
result = account.memberships_by_name
|
|
108
|
+
|
|
109
|
+
result.should == expected
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "is expired with a trial plan after 30 days" do
|
|
113
|
+
trial = Factory(:plan, :trial => true)
|
|
114
|
+
Factory(:account, :created_at => 30.days.ago, :plan => trial).should be_expired
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "isn't expired with a trial plan before 30 days" do
|
|
118
|
+
trial = Factory(:plan, :trial => true)
|
|
119
|
+
Factory(:account, :created_at => 29.days.ago, :plan => trial).should_not be_expired
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "isn't expired with a non-trial plan after 30 days" do
|
|
123
|
+
forever = Factory(:plan, :trial => false)
|
|
124
|
+
Factory(:account, :created_at => 30.days.ago, :plan => forever).should_not be_expired
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "isn't expired without an expiration date after 30 days" do
|
|
128
|
+
trial = Factory(:plan, :trial => true)
|
|
129
|
+
account = Factory(:account, :created_at => 30.days.ago, :plan => trial)
|
|
130
|
+
account.trial_expires_at = nil
|
|
131
|
+
account.save!
|
|
132
|
+
account.should_not be_expired
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "sends notifications for expiring accounts" do
|
|
136
|
+
trial = Factory(:plan, :trial => true)
|
|
137
|
+
forever = Factory(:plan, :trial => false)
|
|
138
|
+
|
|
139
|
+
created_23_days = Factory(:account, :plan => trial, :created_at => 23.days.ago)
|
|
140
|
+
expires_7_days = Factory(:account, :plan => trial, :created_at => 1.day.ago)
|
|
141
|
+
expiring = [created_23_days, expires_7_days]
|
|
142
|
+
forever = Factory(:account, :plan => forever, :created_at => 23.days.ago)
|
|
143
|
+
new_trial = Factory(:account, :plan => trial, :created_at => 22.days.ago)
|
|
144
|
+
already_notified = Factory(:account, :plan => trial,
|
|
145
|
+
:created_at => 24.days.ago,
|
|
146
|
+
:notified_of_expiration => true)
|
|
147
|
+
|
|
148
|
+
expires_7_days.trial_expires_at = 7.days.from_now
|
|
149
|
+
expires_7_days.save!
|
|
150
|
+
|
|
151
|
+
mail = stub('mail', :deliver => true)
|
|
152
|
+
BillingMailer.stubs(:expiring_trial => mail)
|
|
153
|
+
|
|
154
|
+
Account.deliver_expiring_trial_notifications
|
|
155
|
+
|
|
156
|
+
expiring.each do |account|
|
|
157
|
+
BillingMailer.should have_received(:expiring_trial).with(account)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
mail.should have_received(:deliver).twice
|
|
161
|
+
|
|
162
|
+
expiring.each { |account| account.reload.should be_notified_of_expiration }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "sends notifications for completed trials" do
|
|
166
|
+
trial = Factory(:plan, :trial => true)
|
|
167
|
+
forever = Factory(:plan, :trial => false)
|
|
168
|
+
|
|
169
|
+
unexpired_trial = Factory(:account, :plan => trial, :created_at => 29.days.ago)
|
|
170
|
+
unnotified_expired_trial = Factory(:account, :plan => trial, :created_at => 31.days.ago)
|
|
171
|
+
expiring_now = Factory(:account, :plan => trial, :created_at => 1.day.ago)
|
|
172
|
+
notified_expired_trial = Factory(:account, :plan => trial,
|
|
173
|
+
:created_at => 31.days.ago,
|
|
174
|
+
:notified_of_completed_trial => true)
|
|
175
|
+
forever = Factory(:account, :plan => forever, :created_at => 31.days.ago)
|
|
176
|
+
|
|
177
|
+
expiring_now.trial_expires_at = 1.day.ago
|
|
178
|
+
expiring_now.save!
|
|
179
|
+
|
|
180
|
+
requires_notification = [unnotified_expired_trial, expiring_now]
|
|
181
|
+
|
|
182
|
+
mail = stub('mail', :deliver => true)
|
|
183
|
+
BillingMailer.stubs(:completed_trial => mail)
|
|
184
|
+
|
|
185
|
+
Account.deliver_completed_trial_notifications
|
|
186
|
+
|
|
187
|
+
requires_notification.each do |account|
|
|
188
|
+
BillingMailer.should have_received(:completed_trial).with(account)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
mail.should have_received(:deliver).twice
|
|
192
|
+
|
|
193
|
+
requires_notification.each { |account| account.reload.should be_notified_of_completed_trial }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "sends notifications for unactivated accounts after 7 days" do
|
|
197
|
+
unactivated = [Factory(:account, :created_at => 7.days.ago),
|
|
198
|
+
Factory(:account, :created_at => 8.days.ago)]
|
|
199
|
+
fresh = Factory(:account, :created_at => 6.days.ago)
|
|
200
|
+
activated = Factory(:account, :created_at => 9.days.ago, :activated => true)
|
|
201
|
+
already_notified = Factory(:account, :created_at => 9.days.ago,
|
|
202
|
+
:asked_to_activate => true)
|
|
203
|
+
|
|
204
|
+
mail = stub('mail', :deliver => true)
|
|
205
|
+
BillingMailer.stubs(:new_unactivated => mail)
|
|
206
|
+
|
|
207
|
+
Account.deliver_new_unactivated_notifications
|
|
208
|
+
|
|
209
|
+
unactivated.each do |account|
|
|
210
|
+
BillingMailer.should have_received(:new_unactivated).with(account)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
mail.should have_received(:deliver).twice
|
|
214
|
+
|
|
215
|
+
unactivated.each { |account| account.reload.should be_asked_to_activate }
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Invitation do
|
|
4
|
+
it { should validate_presence_of(:account_id) }
|
|
5
|
+
it { should validate_presence_of(:email) }
|
|
6
|
+
it { should belong_to(:account) }
|
|
7
|
+
it { should belong_to(:sender) }
|
|
8
|
+
it { should have_and_belong_to_many(:projects) }
|
|
9
|
+
|
|
10
|
+
it { should_not allow_mass_assignment_of(:account_id) }
|
|
11
|
+
it { should_not allow_mass_assignment_of(:used) }
|
|
12
|
+
|
|
13
|
+
%w(new_user_name new_user_email new_user_password authenticating_user_password).each do |attribute|
|
|
14
|
+
it "allows assignment of #{attribute}" do
|
|
15
|
+
should respond_to(attribute)
|
|
16
|
+
should respond_to(:"#{attribute}=")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe Invitation, "saved" do
|
|
22
|
+
let(:mail) { stub('invitation', :deliver => true) }
|
|
23
|
+
subject { Factory(:invitation) }
|
|
24
|
+
let(:email) { subject.email }
|
|
25
|
+
let(:code) { 'abchex123' }
|
|
26
|
+
|
|
27
|
+
before do
|
|
28
|
+
SecureRandom.stubs(:hex => code)
|
|
29
|
+
InvitationMailer.stubs(:invitation => mail)
|
|
30
|
+
subject
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "sends an invitation email" do
|
|
34
|
+
InvitationMailer.should have_received(:invitation).with(subject)
|
|
35
|
+
mail.should have_received(:deliver)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "delegates account name" do
|
|
39
|
+
subject.account_name.should == subject.account.name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "defauls new user email to invited email" do
|
|
43
|
+
subject.new_user_email.should == subject.email
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "defauls existing user email to invited email" do
|
|
47
|
+
subject.authenticating_user_email.should == subject.email
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "generates a code" do
|
|
51
|
+
SecureRandom.should have_received(:hex).with(8)
|
|
52
|
+
subject.code.should == code
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "uses the code in the url" do
|
|
56
|
+
subject.to_param.should == code
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "delegates sender email" do
|
|
60
|
+
subject.sender_email.should == subject.sender.email
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "delegates sender name" do
|
|
64
|
+
subject.sender_name.should == subject.sender.name
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe Invitation, "valid accept for a new user" do
|
|
69
|
+
let(:account) { Factory(:account) }
|
|
70
|
+
let(:projects) { [Factory(:project, :account => account)] }
|
|
71
|
+
let(:password) { 'secret' }
|
|
72
|
+
let(:name) { 'Rocket' }
|
|
73
|
+
subject { Factory(:invitation, :account => account, :projects => projects) }
|
|
74
|
+
|
|
75
|
+
let!(:result) do
|
|
76
|
+
subject.accept(:new_user_password => password, :new_user_name => name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
let(:user) { subject.user }
|
|
80
|
+
|
|
81
|
+
it "returns true" do
|
|
82
|
+
result.should be_true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "creates a saved, confirmed user" do
|
|
86
|
+
user.should_not be_nil
|
|
87
|
+
user.should be_persisted
|
|
88
|
+
user.name.should == name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "adds the user to the account" do
|
|
92
|
+
account.users.should include(user)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "adds the user to each of the invitation's projects" do
|
|
96
|
+
projects.each do |project|
|
|
97
|
+
user.should be_member_of(project)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "marks the invitation as used" do
|
|
102
|
+
subject.reload.should be_used
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe Invitation, "invalid accept for a new user" do
|
|
107
|
+
subject { Factory(:invitation) }
|
|
108
|
+
let!(:result) { subject.accept({}) }
|
|
109
|
+
let(:user) { subject.user }
|
|
110
|
+
let(:account) { subject.account }
|
|
111
|
+
|
|
112
|
+
it "returns false" do
|
|
113
|
+
result.should be_false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "doesn't create a user" do
|
|
117
|
+
user.should be_new_record
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "adds error messages" do
|
|
121
|
+
subject.errors[:new_user_password].should be_present
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "doesn't mark the invitation as used" do
|
|
125
|
+
subject.reload.should_not be_used
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe Invitation, "valid accept for an existing user authenticating" do
|
|
130
|
+
let(:password) { 'secret' }
|
|
131
|
+
let(:user) { Factory(:user, :password => password) }
|
|
132
|
+
subject { Factory(:invitation, :email => user.email) }
|
|
133
|
+
let(:account) { subject.account }
|
|
134
|
+
|
|
135
|
+
let!(:result) do
|
|
136
|
+
subject.accept(:authenticating_user_password => password)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "returns true" do
|
|
140
|
+
result.should be_true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "adds the user to the account" do
|
|
144
|
+
account.users.should include(user)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "marks the invitation as used" do
|
|
148
|
+
subject.reload.should be_used
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe Invitation, "accepting with an invalid password" do
|
|
153
|
+
let(:user) { Factory(:user) }
|
|
154
|
+
subject { Factory(:invitation, :email => user.email) }
|
|
155
|
+
let(:account) { subject.account }
|
|
156
|
+
let!(:result) { subject.accept(:authenticating_user_password => 'wrong') }
|
|
157
|
+
|
|
158
|
+
it "adds error messages" do
|
|
159
|
+
subject.errors[:authenticating_user_password].should be_present
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "doesn't add the user to the account" do
|
|
163
|
+
subject.account.users.should_not include(subject.user)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "returns false" do
|
|
167
|
+
result.should be_false
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe Invitation, "valid accept for an existing user specifically set" do
|
|
172
|
+
let(:user) { Factory(:user) }
|
|
173
|
+
subject { Factory(:invitation, :email => user.email) }
|
|
174
|
+
let(:account) { subject.account }
|
|
175
|
+
|
|
176
|
+
let!(:result) do
|
|
177
|
+
subject.accept({:existing_user => user})
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "returns true" do
|
|
181
|
+
result.should be_true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "adds the user to the account" do
|
|
185
|
+
account.users.should include(user)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "marks the invitation as used" do
|
|
189
|
+
subject.reload.should be_used
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe Invitation, "saved" do
|
|
194
|
+
let(:mail) { stub('invitation', :deliver => true) }
|
|
195
|
+
subject { Factory(:invitation) }
|
|
196
|
+
let(:email) { subject.email }
|
|
197
|
+
let(:code) { 'abchex123' }
|
|
198
|
+
|
|
199
|
+
before do
|
|
200
|
+
SecureRandom.stubs(:hex => code)
|
|
201
|
+
InvitationMailer.stubs(:invitation => mail)
|
|
202
|
+
subject
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "sends an invitation email" do
|
|
206
|
+
InvitationMailer.should have_received(:invitation).with(subject)
|
|
207
|
+
mail.should have_received(:deliver)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "delegates account name" do
|
|
211
|
+
subject.account_name.should == subject.account.name
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "defauls new user email to invited email" do
|
|
215
|
+
subject.new_user_email.should == subject.email
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "defauls existing user email to invited email" do
|
|
219
|
+
subject.authenticating_user_email.should == subject.email
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it "generates a code" do
|
|
223
|
+
SecureRandom.should have_received(:hex).with(8)
|
|
224
|
+
subject.code.should == code
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "uses the code in the url" do
|
|
228
|
+
subject.to_param.should == code
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
describe Invitation, "valid accept for a new user" do
|
|
233
|
+
let(:account) { Factory(:account) }
|
|
234
|
+
let(:projects) { [Factory(:project, :account => account)] }
|
|
235
|
+
let(:password) { 'secret' }
|
|
236
|
+
let(:name) { 'Rocket' }
|
|
237
|
+
subject { Factory(:invitation, :account => account, :projects => projects) }
|
|
238
|
+
|
|
239
|
+
let!(:result) do
|
|
240
|
+
subject.accept(:new_user_password => password, :new_user_name => name)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
let(:user) { subject.user }
|
|
244
|
+
|
|
245
|
+
it "returns true" do
|
|
246
|
+
result.should be_true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "creates a saved, confirmed user" do
|
|
250
|
+
user.should_not be_nil
|
|
251
|
+
user.should be_persisted
|
|
252
|
+
user.name.should == name
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "adds the user to the account" do
|
|
256
|
+
account.users.should include(user)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it "adds the user to each of the invitation's projects" do
|
|
260
|
+
projects.each do |project|
|
|
261
|
+
user.should be_member_of(project)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "marks the invitation as used" do
|
|
266
|
+
subject.reload.should be_used
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
describe Invitation, "invalid accept for a new user" do
|
|
271
|
+
subject { Factory(:invitation) }
|
|
272
|
+
let!(:result) { subject.accept({}) }
|
|
273
|
+
let(:user) { subject.user }
|
|
274
|
+
let(:account) { subject.account }
|
|
275
|
+
|
|
276
|
+
it "returns false" do
|
|
277
|
+
result.should be_false
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "doesn't create a user" do
|
|
281
|
+
user.should be_new_record
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
it "adds error messages" do
|
|
285
|
+
subject.errors[:new_user_password].should be_present
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "doesn't mark the invitation as used" do
|
|
289
|
+
subject.reload.should_not be_used
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
describe Invitation, "accepting with an unknown email" do
|
|
294
|
+
subject { Factory(:invitation, :email => 'unknown') }
|
|
295
|
+
let(:account) { subject.account }
|
|
296
|
+
let!(:result) { subject.accept(:authenticating_user_password => 'secret') }
|
|
297
|
+
|
|
298
|
+
it "adds error messages" do
|
|
299
|
+
subject.errors[:authenticating_user_email].should be_present
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it "returns false" do
|
|
303
|
+
result.should be_false
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
describe Invitation, "accepting an admin invite" do
|
|
308
|
+
let(:password) { 'secret' }
|
|
309
|
+
let(:user) { Factory(:user, :password => password) }
|
|
310
|
+
subject { Factory(:invitation, :email => user.email, :admin => true) }
|
|
311
|
+
let(:account) { subject.account }
|
|
312
|
+
|
|
313
|
+
let!(:result) do
|
|
314
|
+
subject.accept(:authenticating_user_password => password)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "adds the user as an admin" do
|
|
318
|
+
user.should be_admin_of(account)
|
|
319
|
+
end
|
|
320
|
+
end
|