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