staples 0.1.0 → 1.0.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -1
  3. data/README.md +225 -5
  4. data/lib/staples/cli.rb +25 -2
  5. data/lib/staples/version.rb +3 -1
  6. data/lib/staples.rb +6 -1
  7. data/lib/templates/Procfile +3 -0
  8. data/lib/templates/README.md +127 -0
  9. data/lib/templates/app/controllers/pages_controller.rb +3 -0
  10. data/lib/templates/app/models/membership.rb +4 -0
  11. data/lib/templates/app/models/organization.rb +3 -0
  12. data/lib/templates/app/models/user/account.rb +24 -0
  13. data/lib/templates/app/models/user.rb +3 -0
  14. data/lib/templates/app/views/application/_card.html.erb +5 -0
  15. data/lib/templates/app/views/application/_error_messages.html.erb +11 -0
  16. data/lib/templates/app/views/application/_flashes.html.erb +10 -0
  17. data/lib/templates/app/views/application/_nav.html.erb +27 -0
  18. data/lib/templates/app/views/devise/confirmations/new.html.erb +22 -0
  19. data/lib/templates/app/views/devise/passwords/edit.html.erb +31 -0
  20. data/lib/templates/app/views/devise/passwords/new.html.erb +22 -0
  21. data/lib/templates/app/views/devise/registrations/edit.html.erb +50 -0
  22. data/lib/templates/app/views/devise/registrations/new.html.erb +35 -0
  23. data/lib/templates/app/views/devise/sessions/new.html.erb +32 -0
  24. data/lib/templates/app/views/devise/shared/_links.html.erb +27 -0
  25. data/lib/templates/app/views/pages/home.html.erb +21 -0
  26. data/lib/templates/base.rb +246 -0
  27. data/lib/templates/config/initializers/high_voltage.rb +3 -0
  28. data/lib/templates/config/initializers/sidekiq.rb +12 -0
  29. data/lib/templates/db/migrate/20251121192825_devise_create_users.rb +37 -0
  30. data/lib/templates/db/migrate/20251122141432_create_organizations.rb +7 -0
  31. data/lib/templates/db/migrate/20251122141520_create_memberships.rb +11 -0
  32. data/lib/templates/lib/development/seeder.rb +10 -0
  33. data/lib/templates/lib/tasks/development.rake +15 -0
  34. data/lib/templates/test/controllers/devise/registrations_controller_test.rb +40 -0
  35. data/lib/templates/test/factories/memberships.rb +6 -0
  36. data/lib/templates/test/factories/organizations.rb +4 -0
  37. data/lib/templates/test/factories/users.rb +12 -0
  38. data/lib/templates/test/models/membership_test.rb +13 -0
  39. data/lib/templates/test/models/organization_test.rb +19 -0
  40. data/lib/templates/test/models/user_test.rb +36 -0
  41. data/lib/templates/test/system/authentication_stories_test.rb +72 -0
  42. metadata +36 -1
@@ -0,0 +1,22 @@
1
+ <% content_for :title, "Forgot your password?" %>
2
+
3
+ <h1 id="main_label" class="text-center"><%= content_for :title %></h1>
4
+
5
+ <div class="mx-auto w-100" style="max-width: 400px;">
6
+ <%= render "card" do %>
7
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
8
+ <%= render "error_messages", resource: resource %>
9
+
10
+ <div class="mb-3">
11
+ <%= f.label :email, class: "form-label" %>
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
13
+ </div>
14
+
15
+ <div class="mb-3">
16
+ <%= f.submit "Send me password reset instructions", class: "btn btn-primary" %>
17
+ </div>
18
+ <% end %>
19
+
20
+ <%= render "devise/shared/links" %>
21
+ <% end %>
22
+ </div>
@@ -0,0 +1,50 @@
1
+ <% content_for :title, "Edit #{resource_name.to_s.humanize}" %>
2
+
3
+ <h1 id="main_label" class="text-center"><%= content_for :title %></h1>
4
+
5
+ <div class="mx-auto w-100" style="max-width: 400px;">
6
+ <%= render "card" do %>
7
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
8
+ <%= render "error_messages", resource: resource %>
9
+
10
+ <div class="mb-3">
11
+ <%= f.label :email, class: "form-label" %>
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
13
+ </div>
14
+
15
+ <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
16
+ <div class="alert alert-info">Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
17
+ <% end %>
18
+
19
+ <div class="mb-3">
20
+ <%= f.label :password, class: "form-label" %>
21
+ <div class="form-text">Leave blank if you don't want to change it</div>
22
+ <%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
23
+ <% if @minimum_password_length %>
24
+ <div class="form-text"><%= @minimum_password_length %> characters minimum</div>
25
+ <% end %>
26
+ </div>
27
+
28
+ <div class="mb-3">
29
+ <%= f.label :password_confirmation, class: "form-label" %>
30
+ <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
31
+ </div>
32
+
33
+ <div class="mb-3">
34
+ <%= f.label :current_password, class: "form-label" %>
35
+ <div class="form-text">We need your current password to confirm your changes</div>
36
+ <%= f.password_field :current_password, autocomplete: "current-password", class: "form-control" %>
37
+ </div>
38
+
39
+ <div class="mb-3">
40
+ <%= f.submit "Update", class: "btn btn-primary" %>
41
+ </div>
42
+ <% end %>
43
+
44
+ <h2 class="mt-5">Cancel my account</h2>
45
+
46
+ <div class="mb-3">Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete, class: "btn btn-danger" %></div>
47
+
48
+ <%= link_to "Back", :back, class: "btn btn-link" %>
49
+ <% end %>
50
+ </div>
@@ -0,0 +1,35 @@
1
+ <% content_for :title, "Sign up" %>
2
+
3
+ <h1 id="main_label" class="text-center"><%= content_for :title %></h1>
4
+
5
+ <div class="mx-auto w-100" style="max-width: 400px;">
6
+ <%= render "card" do %>
7
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
8
+ <%= render "error_messages", resource: resource %>
9
+
10
+ <div class="mb-3">
11
+ <%= f.label :email, class: "form-label" %>
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
13
+ </div>
14
+
15
+ <div class="mb-3">
16
+ <%= f.label :password, class: "form-label" %>
17
+ <% if @minimum_password_length %>
18
+ <div class="form-text"><%= @minimum_password_length %> characters minimum</div>
19
+ <% end %>
20
+ <%= f.password_field :password, autocomplete: "new-password", class: "form-control" %>
21
+ </div>
22
+
23
+ <div class="mb-3">
24
+ <%= f.label :password_confirmation, class: "form-label" %>
25
+ <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
26
+ </div>
27
+
28
+ <div class="mb-3">
29
+ <%= f.submit "Sign up", class: "btn btn-primary" %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <%= render "devise/shared/links" %>
34
+ <% end %>
35
+ </div>
@@ -0,0 +1,32 @@
1
+ <% content_for :title, "Log in" %>
2
+
3
+ <h1 id="main_label" class="text-center"><%= content_for :title %></h1>
4
+
5
+ <div class="mx-auto w-100" style="max-width: 400px;">
6
+ <%= render "card" do %>
7
+ <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
8
+ <div class="mb-3">
9
+ <%= f.label :email, class: "form-label" %>
10
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
11
+ </div>
12
+
13
+ <div class="mb-3">
14
+ <%= f.label :password, class: "form-label" %>
15
+ <%= f.password_field :password, autocomplete: "current-password", class: "form-control" %>
16
+ </div>
17
+
18
+ <% if devise_mapping.rememberable? %>
19
+ <div class="mb-3 form-check">
20
+ <%= f.check_box :remember_me, class: "form-check-input" %>
21
+ <%= f.label :remember_me, class: "form-check-label" %>
22
+ </div>
23
+ <% end %>
24
+
25
+ <div class="mb-3">
26
+ <%= f.submit "Log in", class: "btn btn-primary" %>
27
+ </div>
28
+ <% end %>
29
+
30
+ <%= render "devise/shared/links" %>
31
+ <% end %>
32
+ </div>
@@ -0,0 +1,27 @@
1
+ <div class="mt-3">
2
+ <%- if controller_name != 'sessions' %>
3
+ <div class="mb-2"><%= link_to "Log in", new_session_path(resource_name) %></div>
4
+ <% end %>
5
+
6
+ <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
7
+ <div class="mb-2"><%= link_to "Sign up", new_registration_path(resource_name) %></div>
8
+ <% end %>
9
+
10
+ <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
11
+ <div class="mb-2"><%= link_to "Forgot your password?", new_password_path(resource_name) %></div>
12
+ <% end %>
13
+
14
+ <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
15
+ <div class="mb-2"><%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %></div>
16
+ <% end %>
17
+
18
+ <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
19
+ <div class="mb-2"><%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %></div>
20
+ <% end %>
21
+
22
+ <%- if devise_mapping.omniauthable? %>
23
+ <%- resource_class.omniauth_providers.each do |provider| %>
24
+ <div class="mb-2"><%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false }, class: "btn btn-outline-secondary" %></div>
25
+ <% end %>
26
+ <% end %>
27
+ </div>
@@ -0,0 +1,21 @@
1
+ <% content_for :title, "Hello world!" %>
2
+
3
+ <h1 id="main_label"><%= content_for :title %></h1>
4
+
5
+ <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
6
+ Click Here
7
+ </button>
8
+
9
+ <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
10
+ <div class="modal-dialog">
11
+ <div class="modal-content">
12
+ <div class="modal-header">
13
+ <h2 class="modal-title fs-5" id="exampleModalLabel">Hello world!</h2>
14
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
15
+ </div>
16
+ <div class="modal-body">
17
+ If you can read this, Staples has successfully installed.
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
@@ -0,0 +1,246 @@
1
+ def source_paths
2
+ Array(super) + [__dir__]
3
+ end
4
+
5
+ def install_gems
6
+ gem "active_link_to"
7
+ gem "devise", github: "heartcombo/devise"
8
+ gem "high_voltage"
9
+ gem "sidekiq"
10
+ gem "strong_migrations"
11
+
12
+ gem_group :development, :test do
13
+ gem "capybara-email"
14
+ gem "factory_bot_rails"
15
+ end
16
+
17
+ gem_group :test do
18
+ gem "capybara_accessibility_audit"
19
+ end
20
+ end
21
+
22
+ install_gems
23
+
24
+ after_bundle do
25
+ # Generators
26
+ generate_devise
27
+ generate_strong_migrations
28
+
29
+ # Initializers & Configuration
30
+ configure_environments
31
+ configure_database
32
+ add_high_voltage_initializer
33
+ add_sidekiq_initializer
34
+
35
+ # CI
36
+ configure_ci
37
+
38
+ # Database
39
+ add_migrations
40
+
41
+ # Deployment
42
+ add_procfiles
43
+
44
+ # Application Code
45
+ add_application_code
46
+ configure_routes
47
+ add_rake_tasks
48
+
49
+ # Test Suite
50
+ add_test_suite
51
+
52
+ # Assets
53
+ configure_sass
54
+
55
+ # Finalization
56
+ run_migrations
57
+ update_readme
58
+ lint_codebase
59
+
60
+ print_message
61
+ end
62
+
63
+ def generate_devise
64
+ rails_command "generate devise:install"
65
+ gsub_file "config/initializers/devise.rb", /config\.mailer_sender = ['"]please-change-me-at-config-initializers-devise@example\.com['"]/, 'config.mailer_sender = ENV.fetch("MAILER_SENDER", "contact@example.com")'
66
+ end
67
+
68
+ def generate_strong_migrations
69
+ rails_command "generate strong_migrations:install"
70
+ end
71
+
72
+ def configure_ci
73
+ uncomment_lines ".github/workflows/ci.yml", /RAILS_MASTER_KEY/
74
+ end
75
+
76
+ def add_migrations
77
+ copy_file "db/migrate/20251121192825_devise_create_users.rb"
78
+ copy_file "db/migrate/20251122141432_create_organizations.rb"
79
+ copy_file "db/migrate/20251122141520_create_memberships.rb"
80
+ end
81
+
82
+ def add_procfiles
83
+ copy_file "Procfile"
84
+ append_to_file "Procfile.dev", "worker: bundle exec sidekiq -c 10"
85
+ end
86
+
87
+ def configure_database
88
+ gsub_file "config/database.yml", /^production:.*?password:.*?\n/m, <<~YAML
89
+ production:
90
+ <<: *default
91
+ url: <%= ENV["DATABASE_URL"] %>
92
+ YAML
93
+ end
94
+
95
+ def add_application_code
96
+ # Controllers and Pages
97
+ copy_file "app/controllers/pages_controller.rb"
98
+ copy_file "app/views/pages/home.html.erb"
99
+
100
+ # Models
101
+ copy_file "app/models/membership.rb"
102
+ copy_file "app/models/organization.rb"
103
+ copy_file "app/models/user.rb"
104
+ copy_file "app/models/user/account.rb"
105
+
106
+ # Application Partials
107
+ copy_file "app/views/application/_card.html.erb"
108
+ copy_file "app/views/application/_error_messages.html.erb"
109
+ copy_file "app/views/application/_flashes.html.erb"
110
+ copy_file "app/views/application/_nav.html.erb"
111
+
112
+ # Devise Views
113
+ copy_file "app/views/devise/confirmations/new.html.erb"
114
+ copy_file "app/views/devise/passwords/edit.html.erb"
115
+ copy_file "app/views/devise/passwords/new.html.erb"
116
+ copy_file "app/views/devise/registrations/edit.html.erb"
117
+ copy_file "app/views/devise/registrations/new.html.erb"
118
+ copy_file "app/views/devise/sessions/new.html.erb"
119
+ copy_file "app/views/devise/shared/_links.html.erb"
120
+
121
+ # Application Layout
122
+ gsub_file "app/views/layouts/application.html.erb", /<html>/, "<html lang=\"<%= I18n.locale %>\">"
123
+ application_html_erb = <<-ERB
124
+ <%= render "nav" %>
125
+ <main class="container" aria-labelledby="main_label">
126
+ <%= render "flashes" %>
127
+ <%= yield %>
128
+ </main>
129
+ ERB
130
+ gsub_file "app/views/layouts/application.html.erb", /^ <%= yield %>\n/, application_html_erb
131
+ end
132
+
133
+ def configure_environments
134
+ environment "config.active_job.queue_adapter = :sidekiq"
135
+ environment "config.active_record.strict_loading_by_default = true"
136
+ environment "config.active_record.strict_loading_mode = :n_plus_one_only"
137
+ environment "config.require_master_key = true"
138
+
139
+ environment "config.sandbox_by_default = true", env: "production"
140
+ environment "config.active_record.action_on_strict_loading_violation = :log", env: "production"
141
+ gsub_file "config/environments/production.rb", /# config\.asset_host =.*$/, 'config.asset_host = ENV["ASSET_HOST"]'
142
+ gsub_file "config/environments/production.rb", /config\.action_mailer\.default_url_options = \{ host: .*? \}/, 'config.action_mailer.default_url_options = { host: ENV.fetch("APPLICATION_HOST") }'
143
+
144
+ environment "config.active_model.i18n_customize_full_message = true", env: "development"
145
+ uncomment_lines "config/environments/development.rb", /config\.i18n\.raise_on_missing_translations/
146
+ uncomment_lines "config/environments/development.rb", /config\.generators\.apply_rubocop_autocorrect_after_generate!/
147
+
148
+ gsub_file "config/environments/test.rb", /config\.action_dispatch\.show_exceptions = :rescuable/, "config.action_dispatch.show_exceptions = :none"
149
+ gsub_file "config/environments/test.rb", /config\.action_mailer\.default_url_options = \{ host: .*? \}/, "config.action_mailer.default_url_options = { host: \"localhost\", port: 3001 }"
150
+ uncomment_lines "config/environments/test.rb", /config\.i18n\.raise_on_missing_translations/
151
+ environment "config.active_job.queue_adapter = :inline", env: "test"
152
+ end
153
+
154
+ def add_high_voltage_initializer
155
+ copy_file "config/initializers/high_voltage.rb"
156
+ end
157
+
158
+ def add_sidekiq_initializer
159
+ copy_file "config/initializers/sidekiq.rb"
160
+ end
161
+
162
+ def configure_routes
163
+ prepend_to_file "config/routes.rb", "require \"sidekiq/web\"\n\n"
164
+ sidekiq_route = <<-RUBY
165
+ if Rails.env.local?
166
+ mount Sidekiq::Web => "/sidekiq"
167
+ end
168
+
169
+ RUBY
170
+
171
+ insert_into_file "config/routes.rb", sidekiq_route, after: "Rails.application.routes.draw do\n # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html\n"
172
+ route "devise_for :users"
173
+ route 'root to: "pages#show", id: "home"'
174
+ gsub_file "config/routes.rb", / # Defines the root path route.*\n # root "posts#index"\n/, ""
175
+ end
176
+
177
+ def add_rake_tasks
178
+ copy_file "lib/development/seeder.rb"
179
+ copy_file "lib/tasks/development.rake"
180
+ end
181
+
182
+ def add_test_suite
183
+ copy_file "test/controllers/devise/registrations_controller_test.rb"
184
+ copy_file "test/factories/memberships.rb"
185
+ copy_file "test/factories/organizations.rb"
186
+ copy_file "test/factories/users.rb"
187
+ copy_file "test/models/membership_test.rb"
188
+ copy_file "test/models/organization_test.rb"
189
+ copy_file "test/models/user_test.rb"
190
+ copy_file "test/system/authentication_stories_test.rb"
191
+
192
+ application_system_test_case = <<-RUBY
193
+
194
+ def sign_in_as(user)
195
+ visit new_user_session_path
196
+
197
+ fill_in "Email", with: user.email
198
+ fill_in "Password", with: user.password
199
+ click_button "Log in"
200
+
201
+ assert_text I18n.translate("devise.sessions.signed_in")
202
+ end
203
+ RUBY
204
+ insert_into_file "test/application_system_test_case.rb", application_system_test_case, before: "end\n"
205
+
206
+ test_helper_config = <<-RUBY
207
+ include Devise::Test::IntegrationHelpers
208
+ include Capybara::Email::DSL
209
+ include FactoryBot::Syntax::Methods
210
+
211
+ Capybara.configure do |config|
212
+ Rails.application.config.action_mailer.default_url_options => { host:, port: }
213
+
214
+ config.server = :puma, { Silent: true }
215
+ config.server_port = port
216
+ config.app_host = "http://\#{host}:\#{port}"
217
+ end
218
+
219
+ RUBY
220
+ insert_into_file "test/test_helper.rb", test_helper_config, after: "class TestCase\n"
221
+ insert_into_file "test/test_helper.rb", "require \"capybara/email\"\n", after: "require \"rails/test_help\"\n"
222
+ end
223
+
224
+ def configure_sass
225
+ gsub_file "package.json", /"build:css:compile": "sass \.\/app\/assets\/stylesheets\/application\.bootstrap\.scss:\.\/app\/assets\/builds\/application\.css --no-source-map --load-path=node_modules"/, '"build:css:compile": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --silence-deprecation=import --silence-deprecation=global-builtin --silence-deprecation=color-functions"'
226
+ end
227
+
228
+ def run_migrations
229
+ rails_command "db:create"
230
+ rails_command "db:migrate"
231
+ end
232
+
233
+ def update_readme
234
+ remove_file "README.md"
235
+ copy_file "README.md"
236
+ end
237
+
238
+ def lint_codebase
239
+ run "bin/rubocop -a"
240
+ end
241
+
242
+ def print_message
243
+ say "*" * 50
244
+ say "Installation complete! 🎉"
245
+ say "*" * 50
246
+ end
@@ -0,0 +1,3 @@
1
+ HighVoltage.configure do |config|
2
+ config.routes = false
3
+ end
@@ -0,0 +1,12 @@
1
+ SIDEKIQ_REDIS_CONFIGURATION = {
2
+ url: ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), nil), # use REDIS_PROVIDER for Redis environment variable name, defaulting to REDIS_URL
3
+ ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE} # we must trust Heroku and AWS here
4
+ }
5
+
6
+ Sidekiq.configure_server do |config|
7
+ config.redis = SIDEKIQ_REDIS_CONFIGURATION
8
+ end
9
+
10
+ Sidekiq.configure_client do |config|
11
+ config.redis = SIDEKIQ_REDIS_CONFIGURATION
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeviseCreateUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :users do |t|
6
+ ## Database authenticatable
7
+ t.string :email, null: false, default: ""
8
+ t.string :encrypted_password, null: false, default: ""
9
+
10
+ ## Recoverable
11
+ t.string :reset_password_token
12
+ t.datetime :reset_password_sent_at
13
+
14
+ ## Rememberable
15
+ t.datetime :remember_created_at
16
+
17
+ ## Trackable
18
+ t.integer :sign_in_count, default: 0, null: false
19
+ t.datetime :current_sign_in_at
20
+ t.datetime :last_sign_in_at
21
+ t.string :current_sign_in_ip
22
+ t.string :last_sign_in_ip
23
+
24
+ ## Confirmable
25
+ t.string :confirmation_token
26
+ t.datetime :confirmed_at
27
+ t.datetime :confirmation_sent_at
28
+ t.string :unconfirmed_email # Only if using reconfirmable
29
+
30
+ t.timestamps null: false
31
+ end
32
+
33
+ add_index :users, :email, unique: true
34
+ add_index :users, :reset_password_token, unique: true
35
+ add_index :users, :confirmation_token, unique: true
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ class CreateOrganizations < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :organizations do |t|
4
+ t.timestamps
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ class CreateMemberships < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :memberships do |t|
4
+ t.belongs_to :user, null: false, foreign_key: true
5
+ t.belongs_to :organization, null: false, foreign_key: true
6
+ t.index [:user_id, :organization_id], unique: true
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Development
2
+ class Seeder
3
+ def self.load_seeds
4
+ new.load_seeds
5
+ end
6
+
7
+ def load_seeds
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ if Rails.env.local?
2
+ namespace :development do
3
+ namespace :db do
4
+ desc "Loads seed data for the local environment."
5
+ task seed: :environment do
6
+ Development::Seeder.load_seeds
7
+ end
8
+
9
+ namespace :seed do
10
+ desc "Truncate tables of each database for development and loads seed data."
11
+ task replant: ["environment", "db:truncate_all", "development:db:seed"]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ require "test_helper"
2
+
3
+ module Devise
4
+ class RegistrationsController::CreateTest < ActionDispatch::IntegrationTest
5
+ test "creates initial organization on successful registration" do
6
+ attributes_for(:user) => {email:, password:}
7
+
8
+ assert_changes -> { User.count }, from: 0, to: 1 do
9
+ assert_changes -> { Organization.count }, from: 0, to: 1 do
10
+ post user_registration_path, params: {
11
+ user: {
12
+ email:,
13
+ password:,
14
+ password_confirmation: password
15
+ }
16
+ }
17
+ end
18
+ end
19
+
20
+ user = User.sole
21
+ organization = Organization.sole
22
+
23
+ assert_equal organization, user.organization
24
+ end
25
+
26
+ test "does not create initial organization on unsuccessful registration" do
27
+ assert_no_changes -> { User.count }, from: 0 do
28
+ assert_no_changes -> { Organization.count }, from: 0 do
29
+ post user_registration_path, params: {
30
+ user: {
31
+ email: "",
32
+ password: "",
33
+ password_confirmation: ""
34
+ }
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :membership do
3
+ user
4
+ organization
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ FactoryBot.define do
2
+ factory :organization do
3
+ end
4
+ end
@@ -0,0 +1,12 @@
1
+ FactoryBot.define do
2
+ sequence(:user_factory_email) { "person-#{_1}@example.com" }
3
+
4
+ factory :user do
5
+ sequence(:email) { "user-#{_1}@example.com" }
6
+ password { "s3kret" } # avoid 'password', since Chrome will render a security dialog
7
+
8
+ trait :confirmed do
9
+ confirmed_at { Time.current }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require "test_helper"
2
+
3
+ class Membership::DatabaseConstraintTest < ActiveSupport::TestCase
4
+ test "uniqueness constraint" do
5
+ membership = create(:membership)
6
+ user = membership.user
7
+ organization = membership.organization
8
+
9
+ assert_raises ActiveRecord::RecordNotUnique do
10
+ create(:membership, user:, organization:)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require "test_helper"
2
+
3
+ class Organization::DestroyTest < ActiveSupport::TestCase
4
+ test "raises" do
5
+ user = create(:user)
6
+
7
+ assert_raise ActiveRecord::DeleteRestrictionError do
8
+ user.organization.destroy
9
+ end
10
+ end
11
+
12
+ test "destroys" do
13
+ organization = create(:organization)
14
+
15
+ assert_changes -> { Organization.count }, from: 1, to: 0 do
16
+ organization.destroy
17
+ end
18
+ end
19
+ end