Add Users and require Authentication for messaging
Users
In order to increase the variety of validations and form styles in this
example application, introduce the concept of a User, and require that
they be authenticated to create Messages.
When creating Message
records through the MessagesController
,
associate the new messages with the Current user.
Fixtures are declared for existing User
and Message
records to both
simplify writing the authentication tests and so that we can delay a
sign up process until a later point in the process.
Authentications
To authenticate the User, rely on has_secure_password
, the bcrypt
gem, and ActionDispatch
‘s session. The AuthenticationsController
is
responsible for creating, verifying, and destroying User sessions.
Matching before_action
declarations are declared in both
AuthenticationsController
and MessagesController
to ensure that the
proper authentications have occurred before certain actions.
Validations
In a very similar style to the Message
record validations, declare
Authentication
validations for each individual field. Additionally, to
demonstrate a more secure authentication process, when an invalid
username & password pairing are submitted, don’t mark the fields
themselves as invalid, but instead associate the error message to the
record itself.
Note that we’re asserting that the username and password fields are
described by the error message (which relies on aria-describedby
),
and not that they’re validation errors themselves (which rely on the
browser-native Constraint Validation validity state ).
Duplications
At this point in the process, we have two controllers and two forms.
There are some emerging patterns, mostly related to rendering the
correct ARIA attributes, and testing form elements’ Accessibility.
Collapse Gemfile
Expand Gemfile
Gemfile
diff --git a/Gemfile b/Gemfile
index 910c8ef..b6451e0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -21,7 +21,7 @@ gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
-# gem 'bcrypt', '~> 3.1.7'
+gem 'bcrypt', '~> 3.1.7'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
Collapse Gemfile.lock
Expand Gemfile.lock
Gemfile.lock
diff --git a/Gemfile.lock b/Gemfile.lock
index 92a2727..0e4ad44 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -122,6 +122,7 @@ GEM
railties (>= 4.0)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
+ bcrypt (3.1.13)
bindex (0.8.1)
bootsnap (1.4.8)
msgpack (~> 1.0)
@@ -232,6 +233,7 @@ PLATFORMS
DEPENDENCIES
activerecord-session_store
+ bcrypt (~> 3.1.7)
bootsnap (>= 1.4.4)
byebug
capybara (>= 3.26)
Collapse app/controllers/application_controller.rb
Expand app/controllers/application_controller.rb
app/controllers/application_controller.rb
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 6242faf..9ebbc14 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,3 +1,5 @@
class ApplicationController < ActionController::Base
include FlashParams
+
+ before_action { Current.user = Authentication.find(session) }
end
Collapse app/controllers/authentications_controller.rb
Expand app/controllers/authentications_controller.rb
app/controllers/authentications_controller.rb
diff --git a/app/controllers/authentications_controller.rb b/app/controllers/authentications_controller.rb
new file mode 100644
index 0000000..545f6a5
--- /dev/null
+++ b/app/controllers/authentications_controller.rb
@@ -0,0 +1,32 @@
+class AuthenticationsController < ApplicationController
+ before_action(only: :new) { redirect_to messages_url if Current.user }
+
+ def new
+ authentication = Authentication.restore_submission(authentications_params)
+
+ render locals: { authentication: authentication }
+ end
+
+ def create
+ authentication = Authentication.new(authentications_params)
+ authentication.session = session
+
+ if authentication.save
+ redirect_to messages_url
+ else
+ redirect_to new_authentication_url, params: { authentication: authentications_params.except(:password) }
+ end
+ end
+
+ def destroy
+ authentication = Authentication.new(session: session)
+
+ authentication.destroy
+
+ redirect_back fallback_location: messages_url
+ end
+
+ def authentications_params
+ params.fetch(:authentication, {}).permit(:username, :password)
+ end
+end
Collapse app/controllers/messages_controller.rb
Expand app/controllers/messages_controller.rb
app/controllers/messages_controller.rb
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 27fd30a..9dbec88 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -1,6 +1,8 @@
class MessagesController < ApplicationController
+ before_action(except: :index) { head :forbidden unless Current.user }
+
def create
- message = Message.new(message_params)
+ message = Current.user.messages.new(message_params)
if message.save
redirect_to messages_url
@@ -10,7 +12,7 @@ class MessagesController < ApplicationController
end
def index
- messages = Message.all
+ messages = Message.all.order(:created_at).with_rich_text_content
message = Message.restore_submission(message_params)
render locals: { messages: messages, message: message }
Collapse app/models/authentication.rb
Expand app/models/authentication.rb
app/models/authentication.rb
diff --git a/app/models/authentication.rb b/app/models/authentication.rb
new file mode 100644
index 0000000..6fb6e80
--- /dev/null
+++ b/app/models/authentication.rb
@@ -0,0 +1,35 @@
+class Authentication
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include Restorable
+
+ attribute :username, :string
+ attribute :password, :string
+ attribute :session
+
+ validates :username, presence: true
+ validates :password, presence: true
+ validate { errors.add(:base, :invalid) unless user.present? }
+
+ def self.find(session)
+ if (user_id = session[:user_id])
+ User.find_by(id: user_id).tap { |user| session.delete(:user_id) if user.nil? }
+ end
+ end
+
+ def user
+ if (user = User.find_by(username: username)) && user.authenticate_password(password)
+ user
+ end
+ end
+
+ def save
+ if valid?
+ session[:user_id] = user.id
+ end
+ end
+
+ def destroy
+ session.delete(:user_id)
+ end
+end
Collapse app/models/current.rb
Expand app/models/current.rb
app/models/current.rb
diff --git a/app/models/current.rb b/app/models/current.rb
new file mode 100644
index 0000000..73a9744
--- /dev/null
+++ b/app/models/current.rb
@@ -0,0 +1,3 @@
+class Current < ActiveSupport::CurrentAttributes
+ attribute :user
+end
Collapse app/models/message.rb
Expand app/models/message.rb
app/models/message.rb
diff --git a/app/models/message.rb b/app/models/message.rb
index 1ed7734..be736b2 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -1,4 +1,5 @@
class Message < ApplicationRecord
+ belongs_to :user
has_rich_text :content
validates :content, presence: true
Collapse app/models/user.rb
Expand app/models/user.rb
app/models/user.rb
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..0f72fbf
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,5 @@
+class User < ApplicationRecord
+ has_secure_password
+
+ has_many :messages
+end
Collapse app/views/authentications/new.html.erb
Expand app/views/authentications/new.html.erb
app/views/authentications/new.html.erb
diff --git a/app/views/authentications/new.html.erb b/app/views/authentications/new.html.erb
new file mode 100644
index 0000000..fb2f95a
--- /dev/null
+++ b/app/views/authentications/new.html.erb
@@ -0,0 +1,31 @@
+<%= form_with(model: authentication) do |form| %>
+ <% form.object.errors[:base].tap do |errors| %>
+ <% if errors.any? %>
+ <%= tag.span errors.to_sentence, aria: { live: "assertive" } %>
+ <% end %>
+ <% end %>
+
+ <% form.object.errors[:username].tap do |errors| %>
+ <%= form.label :username %>
+ <%= form.text_field :username, aria: {
+ describedby: {form.field_id(:username, :validation_message) => errors.any?},
+ invalid: errors.any?
+ } %>
+ <% if errors.any? %>
+ <%= tag.span errors.to_sentence, id: form.field_id(:username, :validation_message) %>
+ <% end %>
+ <% end %>
+
+ <% form.object.errors[:password].tap do |errors| %>
+ <%= form.label :password %>
+ <%= form.password_field :password, aria: {
+ describedby: {form.field_id(:password, :validation_message) => errors.any?},
+ invalid: errors.any?
+ } %>
+ <% if errors.any? %>
+ <%= tag.span errors.to_sentence, id: form.field_id(:password, :validation_message) %>
+ <% end %>
+ <% end %>
+
+ <%= form.button %>
+<% end %>
Collapse app/views/messages/index.html.erb
Expand app/views/messages/index.html.erb
app/views/messages/index.html.erb
diff --git a/app/views/messages/index.html.erb b/app/views/messages/index.html.erb
index 4f44a89..49bfab9 100644
--- a/app/views/messages/index.html.erb
+++ b/app/views/messages/index.html.erb
@@ -4,17 +4,27 @@
<% end %>
</ul>
-<%= form_with model: message do |form| %>
- <% form.object.errors[:content].tap do |errors| %>
- <%= form.label :content %>
- <%= form.rich_text_area :content, aria: {
- describedby: {message_content_validation_message: errors.any?},
- invalid: errors.any?
- } %>
- <% if errors.any? %>
- <%= tag.span errors.to_sentence, id: :message_content_validation_message %>
+<% if Current.user %>
+ <%= form_with model: message do |form| %>
+ <% form.object.errors[:content].tap do |errors| %>
+ <%= form.label :content %>
+ <%= form.rich_text_area :content, aria: {
+ describedby: {form.field_id(:content, :validation_message) => errors.any?},
+ invalid: errors.any?
+ } %>
+ <% if errors.any? %>
+ <%= tag.span errors.to_sentence, id: form.field_id(:content, :validation_message) %>
+ <% end %>
<% end %>
+
+ <%= form.button %>
<% end %>
- <%= form.button %>
+ <%= button_to authentication_path, method: :delete do %>
+ Log out
+ <% end %>
+<% else %>
+ <%= link_to new_authentication_path do %>
+ Log in to send a message
+ <% end %>
<% end %>
Collapse bin/setup
Expand bin/setup
bin/setup
diff --git a/bin/setup b/bin/setup
index 90700ac..5b66c83 100755
--- a/bin/setup
+++ b/bin/setup
@@ -26,7 +26,7 @@ FileUtils.chdir APP_ROOT do
# end
puts "\n== Preparing database =="
- system! 'bin/rails db:prepare'
+ system! 'bin/rails db:prepare db:fixtures:load'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
Collapse config/locales/en.yml
Expand config/locales/en.yml
config/locales/en.yml
diff --git a/config/locales/en.yml b/config/locales/en.yml
index a56856b..4b4e89b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -32,8 +32,13 @@
en:
helpers:
label:
+ authentication:
+ username: "Username"
+ password: "Password"
message:
content: "Content"
submit:
+ authentication:
+ create: "Log in"
message:
create: "Send"
Collapse config/routes.rb
Expand config/routes.rb
config/routes.rb
diff --git a/config/routes.rb b/config/routes.rb
index 37345a4..e7b857e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,8 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
resources :messages, only: [:index, :create]
+ resources :authentications, only: [:new, :create]
+ resource :authentication, only: :destroy
root to: redirect("/messages")
end
Collapse db/migrate/20201004040718_create_users.rb
Expand db/migrate/20201004040718_create_users.rb
db/migrate/20201004040718_create_users.rb
diff --git a/db/migrate/20201004040718_create_users.rb b/db/migrate/20201004040718_create_users.rb
new file mode 100644
index 0000000..4710f5f
--- /dev/null
+++ b/db/migrate/20201004040718_create_users.rb
@@ -0,0 +1,12 @@
+class CreateUsers < ActiveRecord::Migration[6.1]
+ def change
+ create_table :users do |t|
+ t.text :username, null: false
+ t.text :password_digest, null: false
+
+ t.timestamps
+
+ t.index :username
+ end
+ end
+end
Collapse db/migrate/20201004041613_add_user_to_messages.rb
Expand db/migrate/20201004041613_add_user_to_messages.rb
db/migrate/20201004041613_add_user_to_messages.rb
diff --git a/db/migrate/20201004041613_add_user_to_messages.rb b/db/migrate/20201004041613_add_user_to_messages.rb
new file mode 100644
index 0000000..b71d80a
--- /dev/null
+++ b/db/migrate/20201004041613_add_user_to_messages.rb
@@ -0,0 +1,5 @@
+class AddUserToMessages < ActiveRecord::Migration[6.1]
+ def change
+ add_reference :messages, :user, null: false, foreign_key: true, index: true
+ end
+end
Collapse db/schema.rb
Expand db/schema.rb
db/schema.rb
diff --git a/db/schema.rb b/db/schema.rb
index 8fd5890..65fdfb1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_10_04_015757) do
+ActiveRecord::Schema.define(version: 2020_10_04_041613) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -56,6 +56,8 @@ ActiveRecord::Schema.define(version: 2020_10_04_015757) do
create_table "messages", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
+ t.bigint "user_id", null: false
+ t.index ["user_id"], name: "index_messages_on_user_id"
end
create_table "sessions", force: :cascade do |t|
@@ -67,6 +69,15 @@ ActiveRecord::Schema.define(version: 2020_10_04_015757) do
t.index ["updated_at"], name: "index_sessions_on_updated_at"
end
+ create_table "users", force: :cascade do |t|
+ t.text "username", null: false
+ t.text "password_digest", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["username"], name: "index_users_on_username"
+ end
+
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "messages", "users"
end
Collapse test/application_system_test_case.rb
Expand test/application_system_test_case.rb
test/application_system_test_case.rb
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index 78287d7..0271970 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -6,4 +6,25 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
Capybara.modify_selector(:rich_text_area) do
filter_set(:capybara_accessible_selectors, %i[focused fieldset described_by validation_error])
end
+
+ private
+ def sign_in_as(user)
+ username =
+ case user when Symbol then users(user).username
+ else user.username
+ end
+
+ visit new_authentication_path
+ fill_in label(:authentication, :username), with: username
+ fill_in label(:authentication, :password), with: "password"
+ click_on submit(:authentication)
+ end
+
+ def label(i18n_key, attribute)
+ I18n.translate(attribute, scope: [:helpers, :label, i18n_key])
+ end
+
+ def submit(i18n_key)
+ I18n.translate(:create, scope: [:helpers, :submit, i18n_key])
+ end
end
Collapse test/controllers/authentications_controller_test.rb
Expand test/controllers/authentications_controller_test.rb
test/controllers/authentications_controller_test.rb
diff --git a/test/controllers/authentications_controller_test.rb b/test/controllers/authentications_controller_test.rb
new file mode 100644
index 0000000..659e9bf
--- /dev/null
+++ b/test/controllers/authentications_controller_test.rb
@@ -0,0 +1,65 @@
+require "test_helper"
+
+class AuthenticationsControllerTest < ActionDispatch::IntegrationTest
+ test "#new redirects to messages#index when already authenticated" do
+ sign_in_as :alice
+ get new_authentication_path
+
+ assert_redirected_to messages_url
+ end
+
+ test "#create with a valid username/password pairing writes to the session" do
+ alice = users(:alice)
+
+ sign_in_as alice
+
+ assert_redirected_to messages_url
+ assert_equal alice.id, session[:user_id]
+ end
+
+ test "#create with empty fields redirects with errors" do
+ alice = users(:alice)
+
+ post authentications_path, params: {
+ authentication: {
+ username: "",
+ password: "",
+ }
+ }
+
+ assert_redirected_to(new_authentication_url).then { follow_redirect! }
+ assert_flash_params({ authentication: { username: "" } })
+ assert_nil session[:user_id]
+ assert_select "#authentication_username_validation_message", text: "can't be blank"
+ assert_select %(input[type="text"][aria-invalid="true"][aria-describedby~="authentication_username_validation_message"])
+ assert_select "#authentication_password_validation_message", text: "can't be blank"
+ assert_select %(input[type="password"][aria-invalid="true"][aria-describedby~="authentication_password_validation_message"])
+ end
+
+ test "#create with an invalid username/password pairing redirects with errors" do
+ alice = users(:alice)
+
+ post authentications_path, params: {
+ authentication: {
+ username: alice.username,
+ password: "junk",
+ }
+ }
+
+ assert_redirected_to(new_authentication_url).then { follow_redirect! }
+ assert_flash_params({ authentication: { username: alice.username } })
+ assert_nil session[:user_id]
+ assert_select %(span[aria-live="assertive"]), text: "is invalid"
+ assert_select %(input[type="text"][value="#{alice.username}"])
+ assert_select %(input[type="password"]:not([value]))
+ end
+
+ test "#destroy logs the User out" do
+ sign_in_as :alice
+
+ delete authentication_path
+
+ assert_redirected_to messages_url
+ assert_nil session[:user_id]
+ end
+end
Collapse test/controllers/messages_controller_test.rb
Expand test/controllers/messages_controller_test.rb
test/controllers/messages_controller_test.rb
diff --git a/test/controllers/messages_controller_test.rb b/test/controllers/messages_controller_test.rb
index 0c8edf8..7b91666 100644
--- a/test/controllers/messages_controller_test.rb
+++ b/test/controllers/messages_controller_test.rb
@@ -2,6 +2,7 @@ require "test_helper"
class MessagesControllerTest < ActionDispatch::IntegrationTest
test "#create with invalid parameters redirects to the new action" do
+ sign_in_as :alice
post messages_path, params: {
message: { content: "" }
}
@@ -11,6 +12,7 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
end
test "#create clears parameters once successful" do
+ sign_in_as :alice
post messages_path, params: {
message: { content: "" }
}
@@ -26,7 +28,16 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
end
end
- test "#index through a direct request renders a fresh Message form" do
+ test "#index when unauthenticated links to the log in page" do
+ get messages_path
+
+ assert_response :success
+ assert_select "trix-editor", count: 0
+ assert_select %(a[href="#{new_authentication_path}"])
+ end
+
+ test "#index through a direct authenticated request renders a fresh Message form" do
+ sign_in_as :alice
get messages_path
assert_response :success
@@ -35,6 +46,7 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
end
test "#index when redirected with invalid parameters renders errors" do
+ sign_in_as :alice
post messages_path, params: {
message: { content: "" }
}
@@ -43,13 +55,4 @@ class MessagesControllerTest < ActionDispatch::IntegrationTest
assert_select "#message_content_validation_message", "can't be blank"
assert_select %{trix-editor[aria-invalid="true"][aria-describedby~="message_content_validation_message"]}
end
-
- private
- def assert_flash_params(params)
- values_from_flash = flash[:params].slice(*params.keys)
-
- assert_equal \
- params.with_indifferent_access,
- values_from_flash.with_indifferent_access
- end
end
Collapse test/fixtures/action_text/rich_texts.yml
Expand test/fixtures/action_text/rich_texts.yml
test/fixtures/action_text/rich_texts.yml
diff --git a/test/fixtures/action_text/rich_texts.yml b/test/fixtures/action_text/rich_texts.yml
new file mode 100644
index 0000000..a39967e
--- /dev/null
+++ b/test/fixtures/action_text/rich_texts.yml
@@ -0,0 +1,12 @@
+DEFAULTS: &DEFAULTS
+ record: $LABEL (Message)
+
+alice_hello_bob:
+ <<: *DEFAULTS
+ name: content
+ body: <div>Hello, Bob</div>
+
+bob_hello_alice:
+ <<: *DEFAULTS
+ name: content
+ body: <div>Hello, Alice</div>
Collapse test/fixtures/messages.yml
Expand test/fixtures/messages.yml
test/fixtures/messages.yml
diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml
new file mode 100644
index 0000000..56ffaa6
--- /dev/null
+++ b/test/fixtures/messages.yml
@@ -0,0 +1,7 @@
+alice_hello_bob:
+ created_at: <%= 5.minutes.ago %>
+ user: alice
+
+bob_hello_alice:
+ created_at: <%= 10.minutes.ago %>
+ user: bob
Collapse test/fixtures/users.yml
Expand test/fixtures/users.yml
test/fixtures/users.yml
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644
index 0000000..bfde22d
--- /dev/null
+++ b/test/fixtures/users.yml
@@ -0,0 +1,9 @@
+DEFAULTS: &DEFAULTS
+ username: $LABEL
+ password_digest: <%= BCrypt::Password.create("password") %>
+
+alice:
+ <<: *DEFAULTS
+
+bob:
+ <<: *DEFAULTS
Collapse test/models/authentication_test.rb
Expand test/models/authentication_test.rb
test/models/authentication_test.rb
diff --git a/test/models/authentication_test.rb b/test/models/authentication_test.rb
new file mode 100644
index 0000000..33ca5dd
--- /dev/null
+++ b/test/models/authentication_test.rb
@@ -0,0 +1,101 @@
+require "test_helper"
+
+class AuthenticationTest < ActiveSupport::TestCase
+ test ".find when the session has an invalid user_id returns nil" do
+ session = { user_id: 0 }
+
+ user = Authentication.find(session)
+
+ assert_nil user
+ end
+
+ test ".find when the session is missing a user_id returns nil" do
+ session = {}
+
+ user = Authentication.find(session)
+
+ assert_nil user
+ end
+
+ test ".find retrieves the User matching user_id" do
+ alice = users(:alice)
+ session = { user_id: alice.id }
+
+ user = Authentication.find(session)
+
+ assert_equal alice, user
+ end
+
+ test "invalid when username is blank" do
+ authentication = Authentication.new(username: "")
+
+ valid = authentication.validate
+
+ assert_not valid
+ assert_includes authentication.errors, :username
+ end
+
+ test "invalid when password is blank" do
+ authentication = Authentication.new(password: "")
+
+ valid = authentication.validate
+
+ assert_not valid
+ assert_includes authentication.errors, :password
+ end
+
+ test "invalid when User does not exist" do
+ authentication = Authentication.new(username: "junk", password: "junk")
+
+ valid = authentication.validate
+
+ assert_not valid
+ assert_includes authentication.errors, :base
+ end
+
+ test "invalid when username and password pairing are incorrect" do
+ alice = users(:alice)
+ authentication = Authentication.new(username: alice.username, password: "junk")
+
+ valid = authentication.validate
+
+ assert_not valid
+ assert_includes authentication.errors, :base
+ end
+
+ test "#save does not write to the Session when invalid" do
+ session = {}
+ authentication = Authentication.new(session: session)
+
+ saved = authentication.save
+
+ assert_not saved
+ assert_not_empty authentication.errors
+ assert_empty session
+ end
+
+ test "#save writes to the Session" do
+ alice = users(:alice)
+ session = {}
+ authentication = Authentication.new(
+ username: alice.username,
+ password: "password",
+ session: session
+ )
+
+ saved = authentication.save
+
+ assert saved
+ assert_empty authentication.errors
+ assert_equal alice.id, session[:user_id]
+ end
+
+ test "#destroy removes the user_id from the session" do
+ session = { user_id: 1 }
+ authentication = Authentication.new(session: session)
+
+ authentication.destroy
+
+ assert_empty session
+ end
+end
Collapse test/system/authentications_test.rb
Expand test/system/authentications_test.rb
test/system/authentications_test.rb
diff --git a/test/system/authentications_test.rb b/test/system/authentications_test.rb
new file mode 100644
index 0000000..c7aa586
--- /dev/null
+++ b/test/system/authentications_test.rb
@@ -0,0 +1,11 @@
+require "application_system_test_case"
+
+class AuthenticationsTest < ApplicationSystemTestCase
+ test "invalid authentication renders errors" do
+ visit new_authentication_path
+ click_on submit(:authentication)
+
+ assert_field label(:authentication, :username), described_by: "can't be blank"
+ assert_field label(:authentication, :password), described_by: "can't be blank"
+ end
+end
Collapse test/system/messages_test.rb
Expand test/system/messages_test.rb
test/system/messages_test.rb
diff --git a/test/system/messages_test.rb b/test/system/messages_test.rb
index f005327..f59e32c 100644
--- a/test/system/messages_test.rb
+++ b/test/system/messages_test.rb
@@ -2,7 +2,7 @@ require "application_system_test_case"
class MessagesTest < ApplicationSystemTestCase
test "invalid messages are rendered as :invalid" do
- visit messages_path
+ sign_in_as :alice
fill_in_rich_text_area label(:message, :content), with: ""
click_on submit(:message)
@@ -13,12 +13,4 @@ class MessagesTest < ApplicationSystemTestCase
def assert_rich_text_area(locator, **options)
assert_selector :rich_text_area, locator, **options
end
-
- def label(i18n_key, attribute)
- I18n.translate(attribute, scope: [:helpers, :label, i18n_key])
- end
-
- def submit(i18n_key)
- I18n.translate(:create, scope: [:helpers, :submit, i18n_key])
- end
end
Collapse test/test_helper.rb
Expand test/test_helper.rb
test/test_helper.rb
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 47b598d..c2795d2 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -11,3 +11,29 @@ class ActiveSupport::TestCase
# Add more helper methods to be used by all tests here...
end
+
+class ActionDispatch::IntegrationTest
+ private
+
+ def sign_in_as(user)
+ username =
+ case user when Symbol then users(user).username
+ else user.username
+ end
+
+ post authentications_path, params: {
+ authentication: {
+ username: username,
+ password: "password"
+ }
+ }
+ end
+
+ def assert_flash_params(params)
+ values_from_flash = flash[:params].slice(*params.keys)
+
+ assert_equal \
+ params.with_indifferent_access,
+ values_from_flash.with_indifferent_access
+ end
+end