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,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe User, "valid" do
4
+ subject { Factory(:user) }
5
+
6
+ it { should validate_presence_of(:name) }
7
+
8
+ it { should have_many(:memberships) }
9
+ it { should have_many(:accounts).through(:memberships) }
10
+ it { should have_many(:permissions) }
11
+ it { should have_many(:projects).through(:permissions) }
12
+
13
+ it "is an admin of an account with an admin membership" do
14
+ account = Factory(:account)
15
+ Factory(:membership, :user => subject, :admin => true, :account => account)
16
+ subject.should be_admin_of(account)
17
+
18
+ subject.memberships.admin.first.account.should == account
19
+ end
20
+
21
+ it "isn't an admin of an account with a non admin membership" do
22
+ account = Factory(:account)
23
+ Factory(:membership, :user => subject, :admin => false, :account => account)
24
+ subject.should_not be_admin_of(account)
25
+ end
26
+
27
+ it "isn't an admin of an account without a membership" do
28
+ account = Factory(:account)
29
+ subject.should_not be_admin_of(account)
30
+ end
31
+
32
+ it "is a member with a membership for the given account" do
33
+ account = Factory(:account)
34
+ Factory(:membership, :user => subject, :account => account)
35
+ subject.should be_member_of(account)
36
+ end
37
+
38
+ it "isn't a member without a membership for the given account" do
39
+ account = Factory(:account)
40
+ other_account = Factory(:account)
41
+ Factory(:membership, :user => subject, :account => other_account)
42
+ subject.should_not be_member_of(account)
43
+ end
44
+
45
+ it "is a member with a membership for the given project" do
46
+ project = Factory(:project)
47
+ membership = Factory(:membership, :user => subject,
48
+ :account => project.account)
49
+ Factory(:permission, :membership => membership,
50
+ :project => project)
51
+ subject.should be_member_of(project)
52
+ end
53
+
54
+ it "isn't a member without a membership for the given project" do
55
+ project = Factory(:project)
56
+ other_project = Factory(:project)
57
+ membership = Factory(:membership, :user => subject,
58
+ :account => other_project.account)
59
+ Factory(:permission, :membership => membership,
60
+ :project => other_project)
61
+ subject.should_not be_member_of(project)
62
+ end
63
+
64
+ it "returns users by name" do
65
+ Factory(:user, :name => "def")
66
+ Factory(:user, :name => "abc")
67
+ Factory(:user, :name => "ghi")
68
+
69
+ User.by_name.map(&:name).should == %w(abc def ghi)
70
+ end
71
+ end
72
+
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Saucy routing extensions" do
4
+ include ActionDispatch::Routing::UrlFor
5
+
6
+ let(:_routes) { ActionDispatch::Routing::RouteSet.new }
7
+
8
+ before do
9
+ _routes.draw do
10
+ resources :accounts
11
+ through :accounts do
12
+ resources :projects
13
+ through :projects do
14
+ resources :discussions
15
+ end
16
+ end
17
+ end
18
+
19
+ extend(_routes.named_routes.module)
20
+ end
21
+
22
+ it "allows a nested member path to be accessed through just the child's name" do
23
+ account = stub('account', :to_param => 'abc')
24
+ project = stub('project', :account => account, :to_param => 'def')
25
+ project_path(project).should == "/accounts/abc/projects/def"
26
+ end
27
+
28
+ it "allows a nested member url to be accessed through just the child's name" do
29
+ account = stub('account', :to_param => 'abc')
30
+ project = stub('project', :account => account, :to_param => 'def')
31
+ project_url(project, :host => 'example.com').
32
+ should == "http://example.com/accounts/abc/projects/def"
33
+ end
34
+
35
+ it "allows a nested collection path to be accessed through just the parent's name" do
36
+ account = stub('account', :to_param => 'abc')
37
+ projects_path(account).should == "/accounts/abc/projects"
38
+ end
39
+
40
+ it "allows a nested new path to be accessed through just the parent's name" do
41
+ account = stub('account', :to_param => 'abc')
42
+ new_project_path(account).should == "/accounts/abc/projects/new"
43
+ end
44
+
45
+ it "allows a doubly nested member path to be access through just the child's name" do
46
+ account = stub('account', :to_param => 'abc')
47
+ project = stub('project', :account => account, :to_param => 'def')
48
+ discussion = stub('discussion', :project => project, :to_param => 'ghi')
49
+ discussion_path(discussion).should == "/accounts/abc/projects/def/discussions/ghi"
50
+ end
51
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe Saucy::Configuration do
4
+ it "has layouts" do
5
+ subject.layouts.should be_a(Saucy::Layouts)
6
+ end
7
+
8
+ it "has a manager_email_address" do
9
+ subject.manager_email_address.should_not be_nil
10
+ end
11
+
12
+ it "has a support_email_address" do
13
+ subject.support_email_address.should_not be_nil
14
+ end
15
+
16
+ it "has a nil merchant_account_id" do
17
+ subject.merchant_account_id.should be_nil
18
+ end
19
+
20
+ it "can listen for events" do
21
+ observer = stub("an observer")
22
+ cleanup_observers do
23
+ Saucy::Configuration.observe(observer)
24
+ Saucy::Configuration.observers.should include(observer)
25
+ end
26
+ end
27
+
28
+ it "can notify observers" do
29
+ observer = stub("an observer", :some_event => nil)
30
+ cleanup_observers do
31
+ Saucy::Configuration.observe(observer)
32
+ Saucy::Configuration.notify("some_event", "some_data")
33
+ observer.should have_received("some_event").with("some_data")
34
+ end
35
+ end
36
+
37
+ it "can assign a manager email address" do
38
+ old_address = subject.manager_email_address
39
+ begin
40
+ subject.manager_email_address = 'newsender@example.com'
41
+ subject.manager_email_address.should == 'newsender@example.com'
42
+ ensure
43
+ subject.manager_email_address = old_address
44
+ end
45
+ end
46
+
47
+ it "can assign a support email address" do
48
+ old_address = subject.support_email_address
49
+ begin
50
+ subject.support_email_address = 'newsender@example.com'
51
+ subject.support_email_address.should == 'newsender@example.com'
52
+ ensure
53
+ subject.support_email_address = old_address
54
+ end
55
+ end
56
+
57
+ def cleanup_observers
58
+ yield
59
+ ensure
60
+ subject.observers.clear
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ Rails.application.routes.draw do
2
+ match "sign_in" => "application#index", :as => "sign_in"
3
+ root :to => "application#index"
4
+ end
5
+
@@ -0,0 +1,39 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ ENV["RAILS_ENV"] ||= 'test'
3
+
4
+ if File.exist?("config/environment.rb")
5
+ require "config/environment"
6
+ else
7
+ require File.expand_path("../environment", __FILE__)
8
+ end
9
+
10
+ require 'rspec/rails'
11
+ require 'shoulda'
12
+ require 'timecop'
13
+
14
+ # Requires supporting ruby files with custom matchers and macros, etc,
15
+ # in spec/support/ and its subdirectories.
16
+ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
17
+
18
+ RSpec.configure do |config|
19
+ # == Mock Framework
20
+ #
21
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
22
+ #
23
+ # config.mock_with :mocha
24
+ # config.mock_with :flexmock
25
+ # config.mock_with :rr
26
+ config.mock_with :mocha
27
+
28
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
29
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
30
+
31
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
32
+ # examples within a transaction, remove the following line or assign false
33
+ # instead of true.
34
+ config.use_transactional_fixtures = true
35
+
36
+ config.backtrace_clean_patterns << %r{gems/}
37
+
38
+ config.include AuthenticationHelpers
39
+ end
@@ -0,0 +1,81 @@
1
+ module AuthenticationHelpers
2
+ def sign_in_as(user)
3
+ controller.current_user = user
4
+ end
5
+
6
+ def sign_in
7
+ sign_in_as(Factory(:user))
8
+ end
9
+
10
+ def sign_out
11
+ controller.current_user = nil
12
+ end
13
+
14
+ def current_user
15
+ controller.current_user
16
+ end
17
+
18
+ def signed_in?
19
+ controller.signed_in?
20
+ end
21
+
22
+ def sign_in_as_admin_of_account(account)
23
+ user = Factory(:user)
24
+ Factory(:membership, :user => user, :account => account, :admin => true)
25
+ sign_in_as user
26
+ end
27
+
28
+ def sign_in_as_non_admin_of_account(account)
29
+ user = Factory(:user)
30
+ Factory(:membership, :user => user, :account => account, :admin => false)
31
+ sign_in_as user
32
+ end
33
+
34
+ def sign_in_as_non_admin_of_project(project)
35
+ user = Factory(:user)
36
+ membership = Factory(:membership, :user => user,
37
+ :account => account,
38
+ :admin => false)
39
+ Factory(:permission, :membership => membership,
40
+ :project => project)
41
+ sign_in_as user
42
+ end
43
+
44
+ def sign_in_as_admin_of_project(project)
45
+ user = Factory(:user)
46
+ membership = Factory(:membership, :user => user,
47
+ :account => account,
48
+ :admin => true)
49
+ Factory(:permission, :membership => membership,
50
+ :project => project)
51
+ sign_in_as user
52
+ end
53
+ end
54
+
55
+ RSpec::Matchers.define :be_signed_in do
56
+ match do |controller|
57
+ @controller = controller
58
+ @controller.signed_in? &&
59
+ (@expected_user.nil? || @expected_user == @controller.current_user)
60
+ end
61
+
62
+ chain :as do |user|
63
+ @expected_user = user
64
+ end
65
+
66
+ failure_message_for_should do
67
+ message = "expected to be signed in"
68
+ message << " as #{@expected_user.inspect}" if @expected_user
69
+ message << " but was "
70
+ if @controller.signed_in?
71
+ message << "signed in as #{@controller.current_user.inspect}"
72
+ else
73
+ message << "not signed in"
74
+ end
75
+ message
76
+ end
77
+
78
+ failure_message_for_should_not do
79
+ "didn't expect to be signed in"
80
+ end
81
+ end
@@ -0,0 +1,56 @@
1
+ module AuthorizationHelpers
2
+ module AccountAdminExampleGroup
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ let(:account) { Factory(:account) }
6
+ before { sign_in_as_admin_of_account(account) }
7
+ end
8
+ end
9
+
10
+ module AccountMemberExampleGroup
11
+ extend ActiveSupport::Concern
12
+ included do
13
+ let(:account) { Factory(:account) }
14
+ before { sign_in_as_non_admin_of_account(account) }
15
+ end
16
+ end
17
+
18
+ module ProjectAdminExampleGroup
19
+ extend ActiveSupport::Concern
20
+ included do
21
+ let(:account) { Factory(:account) }
22
+ let(:project) { Factory(:project, :account => account) }
23
+ before { sign_in_as_admin_of_project(project) }
24
+ end
25
+ end
26
+
27
+ module ProjectMemberExampleGroup
28
+ extend ActiveSupport::Concern
29
+ included do
30
+ let(:account) { Factory(:account) }
31
+ let(:project) { Factory(:project, :account => account) }
32
+ before { sign_in_as_non_admin_of_project(project) }
33
+ end
34
+ end
35
+
36
+ module UserExampleGroup
37
+ extend ActiveSupport::Concern
38
+ included do
39
+ let(:user) { Factory(:user) }
40
+ before { sign_in_as(user) }
41
+ end
42
+ end
43
+ end
44
+
45
+ RSpec.configure do |config|
46
+ config.include AuthorizationHelpers::AccountAdminExampleGroup,
47
+ :as => :account_admin
48
+ config.include AuthorizationHelpers::AccountMemberExampleGroup,
49
+ :as => :account_member
50
+ config.include AuthorizationHelpers::ProjectAdminExampleGroup,
51
+ :as => :project_admin
52
+ config.include AuthorizationHelpers::ProjectMemberExampleGroup,
53
+ :as => :project_member
54
+ config.include AuthorizationHelpers::UserExampleGroup,
55
+ :as => :user
56
+ end
@@ -0,0 +1,7 @@
1
+ require 'saucy/fake_braintree'
2
+
3
+ Braintree::Configuration.logger = Logger.new("tmp/log")
4
+
5
+ RSpec.configure do |config|
6
+ config.after(:each) { FakeBraintree.clear! }
7
+ end
@@ -0,0 +1,55 @@
1
+ module ClearanceMatchers
2
+ class DenyAccessMatcher
3
+ include Shoulda::ActionController::Matchers
4
+
5
+ def initialize(context)
6
+ @context = context
7
+ end
8
+
9
+ def matches?(controller)
10
+ if @method
11
+ @context.__send__(@method, *@args)
12
+ end
13
+
14
+ begin
15
+ if @flash
16
+ controller.should set_the_flash.to(@flash)
17
+ else
18
+ controller.should_not set_the_flash
19
+ end
20
+
21
+ url = controller.__send__(:sign_in_url)
22
+ controller.should redirect_to(url).in_context(@context)
23
+
24
+ true
25
+ rescue RSpec::Expectations::ExpectationNotMetError => failure
26
+ @failure_message = failure.message
27
+ false
28
+ end
29
+ end
30
+
31
+ def flash(flash)
32
+ @flash = flash
33
+ self
34
+ end
35
+
36
+ def on(method, *args)
37
+ @method = method
38
+ @args = args
39
+ self
40
+ end
41
+
42
+ def failure_message_for_should
43
+ @failure_message
44
+ end
45
+ end
46
+
47
+ def deny_access
48
+ DenyAccessMatcher.new(self)
49
+ end
50
+ end
51
+
52
+
53
+ RSpec.configure do |config|
54
+ config.include ClearanceMatchers
55
+ end
@@ -0,0 +1,57 @@
1
+ class RecordedEvent
2
+ attr_reader :name, :data
3
+
4
+ def initialize(name, data)
5
+ @name = name.to_s
6
+ @data = data
7
+ end
8
+
9
+ def inspect
10
+ "<Event:#{name} #{inspect_data}>"
11
+ end
12
+
13
+ def ==(other)
14
+ name == other.name && data == other.data
15
+ end
16
+
17
+ private
18
+
19
+ def inspect_data
20
+ data.inject([]) { |result, (key, value)|
21
+ result << "#{key.inspect} => #{value.inspect.slice(0, 20)}"
22
+ }.join(", ")
23
+ end
24
+ end
25
+
26
+ class RecordingObserver
27
+ attr_reader :events
28
+
29
+ def initialize
30
+ @events = []
31
+ end
32
+
33
+ def method_missing(name, data)
34
+ @events << RecordedEvent.new(name, data)
35
+ end
36
+ end
37
+
38
+ RSpec::Matchers.define :notify_observers do |event_name, data|
39
+ match do |ignored_subject|
40
+ @event = RecordedEvent.new(event_name, data)
41
+ recorder.events.should include(@event)
42
+ end
43
+
44
+ failure_message do
45
+ "Expected event:\n#{@event.inspect}\n\nGot events:\n#{recorder.events.map(&:inspect).join("\n")}"
46
+ end
47
+
48
+ def recorder
49
+ Saucy::Configuration.observers.last
50
+ end
51
+ end
52
+
53
+ RSpec.configure do |config|
54
+ config.before do
55
+ Saucy::Configuration.observers = [RecordingObserver.new]
56
+ end
57
+ end