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.
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