saasy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. data/CHANGELOG.md +114 -0
  2. data/Gemfile +26 -0
  3. data/README.md +118 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/accounts_controller.rb +68 -0
  6. data/app/controllers/billings_controller.rb +25 -0
  7. data/app/controllers/invitations_controller.rb +65 -0
  8. data/app/controllers/memberships_controller.rb +45 -0
  9. data/app/controllers/plans_controller.rb +24 -0
  10. data/app/controllers/profiles_controller.rb +19 -0
  11. data/app/helpers/limits_helper.rb +13 -0
  12. data/app/mailers/billing_mailer.rb +53 -0
  13. data/app/mailers/invitation_mailer.rb +18 -0
  14. data/app/models/invitation.rb +113 -0
  15. data/app/models/limit.rb +49 -0
  16. data/app/models/membership.rb +26 -0
  17. data/app/models/permission.rb +19 -0
  18. data/app/models/signup.rb +163 -0
  19. data/app/views/accounts/_account.html.erb +9 -0
  20. data/app/views/accounts/_blank_slate.html.erb +6 -0
  21. data/app/views/accounts/_projects.html.erb +12 -0
  22. data/app/views/accounts/_subnav.html.erb +10 -0
  23. data/app/views/accounts/edit.html.erb +34 -0
  24. data/app/views/accounts/index.html.erb +9 -0
  25. data/app/views/accounts/new.html.erb +36 -0
  26. data/app/views/billing_mailer/completed_trial.text.erb +13 -0
  27. data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
  28. data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
  29. data/app/views/billing_mailer/problem.html.erb +13 -0
  30. data/app/views/billing_mailer/problem.text.erb +14 -0
  31. data/app/views/billing_mailer/receipt.html.erb +41 -0
  32. data/app/views/billing_mailer/receipt.text.erb +25 -0
  33. data/app/views/billings/_form.html.erb +8 -0
  34. data/app/views/billings/edit.html.erb +13 -0
  35. data/app/views/billings/show.html.erb +29 -0
  36. data/app/views/invitation_mailer/invitation.text.erb +6 -0
  37. data/app/views/invitations/new.html.erb +17 -0
  38. data/app/views/invitations/show.html.erb +22 -0
  39. data/app/views/layouts/saucy.html.erb +36 -0
  40. data/app/views/limits/_meter.html.erb +13 -0
  41. data/app/views/memberships/edit.html.erb +21 -0
  42. data/app/views/memberships/index.html.erb +17 -0
  43. data/app/views/plans/_plan.html.erb +32 -0
  44. data/app/views/plans/_terms.html.erb +15 -0
  45. data/app/views/plans/edit.html.erb +33 -0
  46. data/app/views/plans/index.html.erb +12 -0
  47. data/app/views/profiles/_inputs.html.erb +5 -0
  48. data/app/views/profiles/edit.html.erb +36 -0
  49. data/app/views/projects/_form.html.erb +36 -0
  50. data/app/views/projects/edit.html.erb +22 -0
  51. data/app/views/projects/index.html.erb +28 -0
  52. data/app/views/projects/new.html.erb +13 -0
  53. data/app/views/projects/show.html.erb +0 -0
  54. data/app/views/shared/_project_dropdown.html.erb +55 -0
  55. data/app/views/shared/_saucy_javascript.html.erb +33 -0
  56. data/config/locales/en.yml +37 -0
  57. data/config/routes.rb +19 -0
  58. data/features/run_features.feature +83 -0
  59. data/features/step_definitions/clearance_steps.rb +45 -0
  60. data/features/step_definitions/rails_steps.rb +73 -0
  61. data/features/step_definitions/saucy_steps.rb +8 -0
  62. data/features/support/env.rb +4 -0
  63. data/features/support/file.rb +11 -0
  64. data/lib/generators/saucy/base.rb +18 -0
  65. data/lib/generators/saucy/features/features_generator.rb +91 -0
  66. data/lib/generators/saucy/features/templates/README +3 -0
  67. data/lib/generators/saucy/features/templates/factories.rb +71 -0
  68. data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
  69. data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
  70. data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
  71. data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
  72. data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
  73. data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
  74. data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
  75. data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
  76. data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
  77. data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
  78. data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
  79. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
  80. data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
  81. data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
  82. data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
  83. data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
  84. data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
  85. data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
  86. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
  87. data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
  88. data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
  89. data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
  90. data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
  91. data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
  92. data/lib/generators/saucy/install/install_generator.rb +40 -0
  93. data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
  94. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
  95. data/lib/generators/saucy/install/templates/models/account.rb +3 -0
  96. data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
  97. data/lib/generators/saucy/install/templates/models/project.rb +3 -0
  98. data/lib/generators/saucy/specs/specs_generator.rb +20 -0
  99. data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
  100. data/lib/generators/saucy/views/views_generator.rb +23 -0
  101. data/lib/saucy.rb +10 -0
  102. data/lib/saucy/account.rb +132 -0
  103. data/lib/saucy/account_authorization.rb +67 -0
  104. data/lib/saucy/configuration.rb +29 -0
  105. data/lib/saucy/engine.rb +35 -0
  106. data/lib/saucy/fake_braintree.rb +134 -0
  107. data/lib/saucy/layouts.rb +36 -0
  108. data/lib/saucy/plan.rb +54 -0
  109. data/lib/saucy/project.rb +125 -0
  110. data/lib/saucy/projects_controller.rb +94 -0
  111. data/lib/saucy/railties/tasks.rake +28 -0
  112. data/lib/saucy/routing_extensions.rb +121 -0
  113. data/lib/saucy/subscription.rb +237 -0
  114. data/lib/saucy/user.rb +30 -0
  115. data/spec/controllers/accounts_controller_spec.rb +228 -0
  116. data/spec/controllers/application_controller_spec.rb +32 -0
  117. data/spec/controllers/invitations_controller_spec.rb +215 -0
  118. data/spec/controllers/memberships_controller_spec.rb +117 -0
  119. data/spec/controllers/plans_controller_spec.rb +13 -0
  120. data/spec/controllers/profiles_controller_spec.rb +48 -0
  121. data/spec/controllers/projects_controller_spec.rb +216 -0
  122. data/spec/environment.rb +95 -0
  123. data/spec/layouts_spec.rb +21 -0
  124. data/spec/mailers/billing_mailer_spec.rb +68 -0
  125. data/spec/mailers/invitiation_mailer_spec.rb +19 -0
  126. data/spec/models/account_spec.rb +218 -0
  127. data/spec/models/invitation_spec.rb +320 -0
  128. data/spec/models/limit_spec.rb +70 -0
  129. data/spec/models/membership_spec.rb +37 -0
  130. data/spec/models/permission_spec.rb +30 -0
  131. data/spec/models/plan_spec.rb +81 -0
  132. data/spec/models/project_spec.rb +223 -0
  133. data/spec/models/signup_spec.rb +177 -0
  134. data/spec/models/subscription_spec.rb +481 -0
  135. data/spec/models/user_spec.rb +72 -0
  136. data/spec/route_extensions_spec.rb +51 -0
  137. data/spec/saucy_spec.rb +62 -0
  138. data/spec/scaffold/config/routes.rb +5 -0
  139. data/spec/spec_helper.rb +39 -0
  140. data/spec/support/authentication_helpers.rb +81 -0
  141. data/spec/support/authorization_helpers.rb +56 -0
  142. data/spec/support/braintree.rb +7 -0
  143. data/spec/support/clearance_matchers.rb +55 -0
  144. data/spec/support/notifications.rb +57 -0
  145. data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
  146. 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