96.99% test coverage
This commit is contained in:
@@ -140,4 +140,404 @@ class Admin::InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_redirected_to root_path
|
||||
end
|
||||
|
||||
# Resend action tests
|
||||
test "should resend invitation for pending invitation" do
|
||||
login_as(users(:admin_user))
|
||||
pending_user = users(:pending_invitation)
|
||||
original_token = pending_user.invitation_token
|
||||
original_sent_at = pending_user.invitation_sent_at
|
||||
|
||||
assert_enqueued_emails 1 do
|
||||
put resend_admin_invitation_path(pending_user)
|
||||
end
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
assert_match /Invitation resent to #{pending_user.email}/, flash[:notice]
|
||||
|
||||
pending_user.reload
|
||||
assert_not_equal original_token, pending_user.invitation_token
|
||||
assert_operator pending_user.invitation_sent_at, :>, original_sent_at
|
||||
end
|
||||
|
||||
test "should not resend accepted invitation" do
|
||||
login_as(users(:admin_user))
|
||||
accepted_user = users(:contributor_user)
|
||||
|
||||
assert_enqueued_emails 0 do
|
||||
put resend_admin_invitation_path(accepted_user)
|
||||
end
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
assert_equal "Cannot resend an accepted invitation.", flash[:alert]
|
||||
end
|
||||
|
||||
test "should update invitation_sent_at when resending" do
|
||||
login_as(users(:admin_user))
|
||||
pending_user = users(:pending_invitation)
|
||||
|
||||
freeze_time do
|
||||
put resend_admin_invitation_path(pending_user)
|
||||
|
||||
pending_user.reload
|
||||
assert_in_delta Time.current.to_i, pending_user.invitation_sent_at.to_i, 2
|
||||
end
|
||||
end
|
||||
|
||||
test "should generate new token when resending" do
|
||||
login_as(users(:admin_user))
|
||||
pending_user = users(:pending_invitation)
|
||||
original_token = pending_user.invitation_token
|
||||
|
||||
put resend_admin_invitation_path(pending_user)
|
||||
|
||||
pending_user.reload
|
||||
assert_not_nil pending_user.invitation_token
|
||||
assert_not_equal original_token, pending_user.invitation_token
|
||||
assert pending_user.invitation_token.length >= 32
|
||||
end
|
||||
|
||||
test "should not allow non-admin to resend invitation" do
|
||||
login_as(users(:contributor_user))
|
||||
|
||||
put resend_admin_invitation_path(users(:pending_invitation))
|
||||
|
||||
assert_redirected_to root_path
|
||||
end
|
||||
|
||||
test "should require authentication to resend invitation" do
|
||||
put resend_admin_invitation_path(users(:pending_invitation))
|
||||
|
||||
assert_redirected_to login_path
|
||||
end
|
||||
|
||||
# Index action tests
|
||||
test "should list pending invitations in descending order by sent date" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
# Create multiple pending invitations
|
||||
user1 = User.create!(email: "user1@example.com", name: "User 1", password: "temp123456789")
|
||||
user1.invite_by!(users(:admin_user))
|
||||
|
||||
user2 = User.create!(email: "user2@example.com", name: "User 2", password: "temp123456789")
|
||||
user2.update_columns(invitation_token: "token2", invitation_sent_at: 1.hour.ago)
|
||||
|
||||
get admin_invitations_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "h3", /Pending Invitations/i
|
||||
# Check both emails appear in the page
|
||||
assert_select "td", text: "user1@example.com"
|
||||
assert_select "td", text: "user2@example.com"
|
||||
end
|
||||
|
||||
test "should display accepted invitations section" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
get admin_invitations_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "h3", /Recently Accepted/i
|
||||
end
|
||||
|
||||
test "should show pending invitation count badge" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
get admin_invitations_path
|
||||
|
||||
assert_response :success
|
||||
assert_select "span.bg-yellow-100"
|
||||
end
|
||||
|
||||
test "should display pending invitations table when invitations exist" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
get admin_invitations_path
|
||||
|
||||
assert_select "table"
|
||||
assert_select "th", text: "Email"
|
||||
assert_select "th", text: "Role"
|
||||
assert_select "th", text: "Sent"
|
||||
end
|
||||
|
||||
# Create action - additional tests
|
||||
test "should create invitation with reviewer role" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "reviewer@example.com",
|
||||
name: "New Reviewer",
|
||||
role: "reviewer",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "reviewer@example.com")
|
||||
assert_equal "reviewer", new_user.role
|
||||
end
|
||||
|
||||
test "should create invitation with admin role" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "admin@example.com",
|
||||
name: "New Admin",
|
||||
role: "admin",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "admin@example.com")
|
||||
assert_equal "admin", new_user.role
|
||||
end
|
||||
|
||||
test "should ignore invalid role parameter" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "superadmin",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_equal "contributor", new_user.role
|
||||
end
|
||||
|
||||
test "should set invited_by to current admin" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_equal users(:admin_user).id, new_user.invited_by_id
|
||||
end
|
||||
|
||||
test "should generate random password for new invitation" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert new_user.password_digest.present?
|
||||
end
|
||||
|
||||
test "should not create invitation with duplicate email" do
|
||||
login_as(users(:admin_user))
|
||||
existing_user = users(:admin_user)
|
||||
|
||||
assert_no_difference("User.count") do
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: existing_user.email,
|
||||
name: "Duplicate User",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "div.text-red-700", text: /already been taken/i
|
||||
end
|
||||
|
||||
test "should create invitation even with blank name" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
assert_difference("User.count", 1) do
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_not_nil new_user
|
||||
end
|
||||
|
||||
test "should show success message with email after creating invitation" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
assert_match /Invitation sent to newuser@example\.com/, flash[:notice]
|
||||
end
|
||||
|
||||
# Destroy action - additional tests
|
||||
test "should show success message after cancelling invitation" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
delete admin_invitation_path(users(:pending_invitation))
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
assert_equal "Invitation cancelled.", flash[:notice]
|
||||
end
|
||||
|
||||
test "should delete user record when cancelling invitation" do
|
||||
login_as(users(:admin_user))
|
||||
pending_user = users(:pending_invitation)
|
||||
user_id = pending_user.id
|
||||
|
||||
delete admin_invitation_path(pending_user)
|
||||
|
||||
assert_nil User.find_by(id: user_id)
|
||||
end
|
||||
|
||||
test "should not allow destroying user with entries" do
|
||||
login_as(users(:admin_user))
|
||||
user_with_entries = users(:contributor_user)
|
||||
|
||||
# This user has accepted invitation and potentially has entries
|
||||
assert_no_difference("User.count") do
|
||||
delete admin_invitation_path(user_with_entries)
|
||||
end
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
assert_equal "Cannot cancel an accepted invitation.", flash[:alert]
|
||||
end
|
||||
|
||||
# Security tests
|
||||
test "should only permit email, name, primary_language, and role params" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "contributor",
|
||||
primary_language: "en",
|
||||
password: "hackedpassword123",
|
||||
invitation_token: "hacked_token",
|
||||
invitation_accepted_at: Time.current
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_not_nil new_user
|
||||
# Password should be randomly generated, not from params
|
||||
assert_not new_user.authenticate("hackedpassword123")
|
||||
# Invitation token should be generated by invite_by, not from params
|
||||
assert_not_equal "hacked_token", new_user.invitation_token
|
||||
# Invitation should not be pre-accepted
|
||||
assert_nil new_user.invitation_accepted_at
|
||||
end
|
||||
|
||||
test "should require admin role for all actions" do
|
||||
login_as(users(:reviewer_user))
|
||||
|
||||
# Index
|
||||
get admin_invitations_path
|
||||
assert_redirected_to root_path
|
||||
|
||||
# New
|
||||
get new_admin_invitation_path
|
||||
assert_redirected_to root_path
|
||||
|
||||
# Create
|
||||
post admin_invitations_path, params: { user: { email: "test@example.com", name: "Test" } }
|
||||
assert_redirected_to root_path
|
||||
|
||||
# Resend
|
||||
put resend_admin_invitation_path(users(:pending_invitation))
|
||||
assert_redirected_to root_path
|
||||
|
||||
# Destroy
|
||||
delete admin_invitation_path(users(:pending_invitation))
|
||||
assert_redirected_to root_path
|
||||
end
|
||||
|
||||
# Edge cases
|
||||
test "should handle resending expired invitation" do
|
||||
login_as(users(:admin_user))
|
||||
pending_user = users(:pending_invitation)
|
||||
pending_user.update_columns(invitation_sent_at: 20.days.ago)
|
||||
|
||||
put resend_admin_invitation_path(pending_user)
|
||||
|
||||
assert_redirected_to admin_invitations_path
|
||||
pending_user.reload
|
||||
assert pending_user.invitation_sent_at > 1.hour.ago
|
||||
end
|
||||
|
||||
test "should normalize email to lowercase when creating invitation" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "NewUser@Example.COM",
|
||||
name: "New User",
|
||||
role: "contributor",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_not_nil new_user
|
||||
assert_equal "newuser@example.com", new_user.email
|
||||
end
|
||||
|
||||
test "should handle missing role parameter gracefully" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_not_nil new_user
|
||||
assert_equal "contributor", new_user.role
|
||||
end
|
||||
|
||||
test "should handle blank role parameter" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
post admin_invitations_path, params: {
|
||||
user: {
|
||||
email: "newuser@example.com",
|
||||
name: "New User",
|
||||
role: "",
|
||||
primary_language: "en"
|
||||
}
|
||||
}
|
||||
|
||||
new_user = User.find_by(email: "newuser@example.com")
|
||||
assert_not_nil new_user
|
||||
assert_equal "contributor", new_user.role
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,9 +43,9 @@ class Admin::RequestsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :success
|
||||
assert_select "h1", "Entry Request Details"
|
||||
assert_match @requested_entry.fi, response.body
|
||||
assert_match @requested_entry.en, response.body
|
||||
assert_match @requested_entry.requested_by.name, response.body
|
||||
assert_select "div", text: @requested_entry.fi
|
||||
assert_select "div", text: @requested_entry.en
|
||||
assert_select "span", text: @requested_entry.requested_by.name
|
||||
end
|
||||
|
||||
test "should show edit form for requested entry" do
|
||||
|
||||
@@ -18,6 +18,26 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should filter users by role" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
get admin_users_path, params: { role: "reviewer" }
|
||||
|
||||
assert_response :success
|
||||
assert_select "td", text: /#{Regexp.escape(users(:reviewer_user).email)}/
|
||||
assert_select "td", text: /#{Regexp.escape(users(:contributor_user).email)}/, count: 0
|
||||
end
|
||||
|
||||
test "should filter users by email query" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
get admin_users_path, params: { q: "admin" }
|
||||
|
||||
assert_response :success
|
||||
assert_select "td", text: /#{Regexp.escape(users(:admin_user).email)}/
|
||||
assert_select "td", text: /#{Regexp.escape(users(:contributor_user).email)}/, count: 0
|
||||
end
|
||||
|
||||
test "should get edit page for user when logged in as admin" do
|
||||
login_as(users(:admin_user))
|
||||
get edit_admin_user_path(users(:contributor_user))
|
||||
@@ -35,6 +55,45 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal "reviewer", users(:contributor_user).reload.role
|
||||
end
|
||||
|
||||
test "should not allow admin to update own role" do
|
||||
admin_user = users(:admin_user)
|
||||
login_as(admin_user)
|
||||
|
||||
patch admin_user_path(admin_user), params: {
|
||||
user: { role: "reviewer" }
|
||||
}
|
||||
|
||||
assert_redirected_to admin_users_path
|
||||
assert_equal "You cannot modify your own role.", flash[:alert]
|
||||
assert_equal "admin", admin_user.reload.role
|
||||
end
|
||||
|
||||
test "should ignore invalid role updates" do
|
||||
login_as(users(:admin_user))
|
||||
contributor = users(:contributor_user)
|
||||
|
||||
patch admin_user_path(contributor), params: {
|
||||
user: { role: "invalid_role", name: "Updated Name" }
|
||||
}
|
||||
|
||||
assert_redirected_to admin_users_path
|
||||
contributor.reload
|
||||
assert_equal "contributor", contributor.role
|
||||
assert_equal "Updated Name", contributor.name
|
||||
end
|
||||
|
||||
test "should render edit when update is invalid" do
|
||||
login_as(users(:admin_user))
|
||||
contributor = users(:contributor_user)
|
||||
|
||||
patch admin_user_path(contributor), params: {
|
||||
user: { email: "" }
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "li", text: "Email can't be blank"
|
||||
end
|
||||
|
||||
test "should delete user when logged in as admin" do
|
||||
login_as(users(:admin_user))
|
||||
|
||||
@@ -46,6 +105,37 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to admin_users_path
|
||||
end
|
||||
|
||||
test "should not allow admin to delete own account" do
|
||||
admin_user = users(:admin_user)
|
||||
login_as(admin_user)
|
||||
|
||||
assert_no_difference("User.count") do
|
||||
delete admin_user_path(admin_user)
|
||||
end
|
||||
|
||||
assert_redirected_to admin_users_path
|
||||
assert_equal "You cannot delete your own account.", flash[:alert]
|
||||
end
|
||||
|
||||
test "should not allow deleting first admin user" do
|
||||
other_admin = User.create!(
|
||||
email: "other-admin@example.com",
|
||||
name: "Other Admin",
|
||||
role: :admin,
|
||||
primary_language: "en",
|
||||
password: "password123456",
|
||||
invitation_accepted_at: Time.current
|
||||
)
|
||||
login_as(other_admin)
|
||||
|
||||
assert_no_difference("User.count") do
|
||||
delete admin_user_path(User.first)
|
||||
end
|
||||
|
||||
assert_redirected_to admin_users_path
|
||||
assert_equal "Cannot delete the first admin user (system default contact).", flash[:alert]
|
||||
end
|
||||
|
||||
test "should not allow non-admin to update user" do
|
||||
login_as(users(:contributor_user))
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
require "test_helper"
|
||||
require "roo"
|
||||
require "tempfile"
|
||||
|
||||
class EntriesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@entry = entries(:one)
|
||||
@user = users(:admin_user)
|
||||
end
|
||||
|
||||
# INDEX tests
|
||||
test "should get index" do
|
||||
get entries_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should filter by language" do
|
||||
get entries_url, params: { language: "fi" }
|
||||
assert_response :success
|
||||
assert_select "input[type=hidden][name='language'][value='fi']"
|
||||
end
|
||||
|
||||
test "should filter by category" do
|
||||
get entries_url, params: { category: "word" }
|
||||
assert_response :success
|
||||
assert_select "input[type=hidden][name='category'][value='word']"
|
||||
end
|
||||
|
||||
test "should search with query" do
|
||||
get entries_url, params: { q: "test" }
|
||||
assert_response :success
|
||||
assert_select "input[name='q'][value='test']"
|
||||
end
|
||||
|
||||
test "should filter by starts_with" do
|
||||
get entries_url, params: { starts_with: "a" }
|
||||
assert_response :success
|
||||
assert_select "input[type=hidden][name='starts_with'][value='a']"
|
||||
end
|
||||
|
||||
test "should paginate results" do
|
||||
get entries_url, params: { page: 2 }
|
||||
assert_response :success
|
||||
assert_select "div", text: /Page 2 of/i
|
||||
end
|
||||
|
||||
test "should handle invalid language code" do
|
||||
get entries_url, params: { language: "invalid" }
|
||||
assert_response :success
|
||||
assert_select "input[name='language']", count: 0
|
||||
end
|
||||
|
||||
test "should respond to turbo_stream" do
|
||||
get entries_url, as: :turbo_stream
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should only show active entries in index" do
|
||||
# Create a requested entry that should not appear
|
||||
requested_entry = Entry.create!(
|
||||
fi: "Requested",
|
||||
category: :word,
|
||||
status: :requested,
|
||||
requested_by: @user
|
||||
)
|
||||
|
||||
get entries_url
|
||||
assert_response :success
|
||||
assert_select "td", text: requested_entry.fi, count: 0
|
||||
end
|
||||
|
||||
# SHOW tests
|
||||
test "should show entry" do
|
||||
get entry_url(@entry)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should show entry with comments" do
|
||||
get entry_url(@entry)
|
||||
assert_response :success
|
||||
assert_select "p", text: @entry.fi
|
||||
end
|
||||
|
||||
# EDIT tests
|
||||
test "should get edit" do
|
||||
get edit_entry_url(@entry)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
# UPDATE tests
|
||||
test "should update entry" do
|
||||
patch entry_url(@entry), params: {
|
||||
entry: {
|
||||
fi: "Updated Finnish",
|
||||
en: "Updated English",
|
||||
category: "word"
|
||||
}
|
||||
}
|
||||
assert_redirected_to entry_url(@entry)
|
||||
@entry.reload
|
||||
assert_equal "Updated Finnish", @entry.fi
|
||||
end
|
||||
|
||||
test "should not update entry with invalid data" do
|
||||
patch entry_url(@entry), params: {
|
||||
entry: {
|
||||
fi: "",
|
||||
en: "",
|
||||
sv: "",
|
||||
no: "",
|
||||
ru: "",
|
||||
de: ""
|
||||
}
|
||||
}
|
||||
assert_response :unprocessable_entity
|
||||
end
|
||||
|
||||
test "should update entry category" do
|
||||
patch entry_url(@entry), params: {
|
||||
entry: { category: "phrase" }
|
||||
}
|
||||
assert_redirected_to entry_url(@entry)
|
||||
@entry.reload
|
||||
assert_equal "phrase", @entry.category
|
||||
end
|
||||
|
||||
# DOWNLOAD tests
|
||||
test "should download xlsx" do
|
||||
get download_entries_url(format: :xlsx)
|
||||
assert_response :success
|
||||
assert_equal "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
response.media_type
|
||||
end
|
||||
|
||||
test "should include active entries in xlsx" do
|
||||
unique_entry = Entry.create!(
|
||||
fi: "UniqueActiveEntry123",
|
||||
category: :word,
|
||||
status: :active
|
||||
)
|
||||
|
||||
get download_entries_url(format: :xlsx)
|
||||
assert_response :success
|
||||
|
||||
Tempfile.create([ "entries", ".xlsx" ]) do |file|
|
||||
file.binmode
|
||||
file.write(response.body)
|
||||
file.flush
|
||||
|
||||
sheet = Roo::Excelx.new(file.path).sheet(0)
|
||||
cell_values = sheet.to_a.flatten.compact
|
||||
assert_includes cell_values, unique_entry.fi
|
||||
end
|
||||
end
|
||||
|
||||
# Statistics tests
|
||||
test "should calculate entry statistics" do
|
||||
get entries_url
|
||||
assert_response :success
|
||||
assert_select "div", text: /entries/i
|
||||
assert_select "div", text: /% complete/i
|
||||
end
|
||||
|
||||
test "should calculate language completion" do
|
||||
get entries_url
|
||||
assert_response :success
|
||||
assert_select "div", text: /% complete/i
|
||||
end
|
||||
|
||||
# Language ordering tests
|
||||
test "should prioritize selected language in display" do
|
||||
get entries_url, params: { language: "fi" }
|
||||
assert_response :success
|
||||
assert_select "th span", text: "FI", count: 1
|
||||
end
|
||||
end
|
||||
@@ -90,4 +90,306 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to root_path
|
||||
assert_equal "Invalid or expired invitation link.", flash[:alert]
|
||||
end
|
||||
|
||||
test "should redirect admin to dashboard after accepting invitation" do
|
||||
inviter = users(:admin_user)
|
||||
pending_admin = User.create!(
|
||||
email: "pending-admin@example.com",
|
||||
name: "Pending Admin",
|
||||
role: :admin,
|
||||
primary_language: "en",
|
||||
invitation_token: "pending_admin_token",
|
||||
invitation_sent_at: 1.day.ago,
|
||||
invited_by: inviter,
|
||||
password: "password123456"
|
||||
)
|
||||
|
||||
patch accept_invitation_path(pending_admin.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
pending_admin.reload
|
||||
assert_not_nil pending_admin.invitation_accepted_at
|
||||
assert_nil pending_admin.invitation_token
|
||||
assert_equal pending_admin.id, session[:user_id]
|
||||
assert_redirected_to admin_root_path
|
||||
end
|
||||
|
||||
# Entry activation tests
|
||||
test "should activate approved entries on invitation acceptance" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
# Create approved entry for this user
|
||||
approved_entry = Entry.create!(
|
||||
category: :word,
|
||||
fi: "Test word",
|
||||
status: :approved,
|
||||
requested_by: user
|
||||
)
|
||||
|
||||
# Create requested entry (should not be activated)
|
||||
requested_entry = Entry.create!(
|
||||
category: :word,
|
||||
fi: "Requested word",
|
||||
status: :requested,
|
||||
requested_by: user
|
||||
)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
approved_entry.reload
|
||||
requested_entry.reload
|
||||
|
||||
assert_equal "active", approved_entry.status
|
||||
assert_equal "requested", requested_entry.status
|
||||
end
|
||||
|
||||
test "should activate multiple approved entries on invitation acceptance" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
# Create multiple approved entries
|
||||
entries = 3.times.map do |i|
|
||||
Entry.create!(
|
||||
category: :word,
|
||||
fi: "Word #{i}",
|
||||
status: :approved,
|
||||
requested_by: user
|
||||
)
|
||||
end
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
entries.each do |entry|
|
||||
entry.reload
|
||||
assert_equal "active", entry.status
|
||||
end
|
||||
end
|
||||
|
||||
test "should not activate entries for other users" do
|
||||
user = users(:pending_invitation)
|
||||
other_user = users(:admin_user)
|
||||
|
||||
# Create approved entry for another user
|
||||
other_entry = Entry.create!(
|
||||
category: :word,
|
||||
fi: "Other user word",
|
||||
status: :approved,
|
||||
requested_by: other_user
|
||||
)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
other_entry.reload
|
||||
assert_equal "approved", other_entry.status
|
||||
end
|
||||
|
||||
test "should handle invitation acceptance with no entries" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
# Ensure the user has no entries
|
||||
Entry.where(requested_by: user).delete_all
|
||||
assert_equal 0, Entry.where(requested_by: user).count
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to root_path
|
||||
user.reload
|
||||
assert_not_nil user.invitation_accepted_at
|
||||
end
|
||||
|
||||
# Security tests
|
||||
test "should only permit password parameters" do
|
||||
user = users(:pending_invitation)
|
||||
original_email = user.email
|
||||
original_name = user.name
|
||||
original_role = user.role
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123",
|
||||
email: "hacker@example.com",
|
||||
name: "Hacker Name",
|
||||
role: "admin"
|
||||
}
|
||||
}
|
||||
|
||||
user.reload
|
||||
assert_equal original_email, user.email
|
||||
assert_equal original_name, user.name
|
||||
assert_equal original_role, user.role
|
||||
end
|
||||
|
||||
test "should not log in user when password validation fails" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "short",
|
||||
password_confirmation: "short"
|
||||
}
|
||||
}
|
||||
|
||||
assert_nil session[:user_id]
|
||||
end
|
||||
|
||||
# Edge cases
|
||||
test "should handle already accepted invitation" do
|
||||
user = users(:pending_invitation)
|
||||
user.update!(invitation_accepted_at: Time.current, invitation_token: nil)
|
||||
|
||||
patch accept_invitation_path("some_token"), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to root_path
|
||||
assert_equal "Invalid or expired invitation link.", flash[:alert]
|
||||
end
|
||||
|
||||
test "should set invitation_accepted_at timestamp" do
|
||||
user = users(:pending_invitation)
|
||||
assert_nil user.invitation_accepted_at
|
||||
|
||||
freeze_time do
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
user.reload
|
||||
assert_in_delta Time.current.to_i, user.invitation_accepted_at.to_i, 2
|
||||
end
|
||||
end
|
||||
|
||||
test "should clear invitation token after acceptance" do
|
||||
user = users(:pending_invitation)
|
||||
token = user.invitation_token
|
||||
assert_not_nil token
|
||||
|
||||
patch accept_invitation_path(token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
user.reload
|
||||
assert_nil user.invitation_token
|
||||
end
|
||||
|
||||
test "should require password to be present" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: nil,
|
||||
password_confirmation: nil
|
||||
}
|
||||
}
|
||||
|
||||
# has_secure_password validates password presence when setting password
|
||||
user.reload
|
||||
assert_nil user.invitation_accepted_at
|
||||
end
|
||||
|
||||
test "should show validation errors for short password" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "short",
|
||||
password_confirmation: "short"
|
||||
}
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "div.text-red-700", text: /too short/i
|
||||
end
|
||||
|
||||
test "should show validation errors for mismatched passwords" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "differentpassword"
|
||||
}
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "div.text-red-700", text: /confirmation/i
|
||||
end
|
||||
|
||||
test "should authenticate with new password after acceptance" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "mynewpassword123",
|
||||
password_confirmation: "mynewpassword123"
|
||||
}
|
||||
}
|
||||
|
||||
user.reload
|
||||
assert user.authenticate("mynewpassword123")
|
||||
assert_not user.authenticate("oldpassword")
|
||||
end
|
||||
|
||||
test "should log in user immediately after acceptance" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
# First visit the invitation page to establish session
|
||||
get invitation_path(user.invitation_token)
|
||||
assert_nil session[:user_id]
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal user.id, session[:user_id]
|
||||
end
|
||||
|
||||
test "should show welcome message with user name" do
|
||||
user = users(:pending_invitation)
|
||||
|
||||
patch accept_invitation_path(user.invitation_token), params: {
|
||||
user: {
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
}
|
||||
|
||||
assert_match /Welcome to Sanasto Wiki, #{user.name}!/, flash[:notice]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
require "test_helper"
|
||||
|
||||
class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:admin_user)
|
||||
@user.update!(invitation_accepted_at: Time.current)
|
||||
end
|
||||
|
||||
# NEW tests
|
||||
test "should get new" do
|
||||
get new_password_reset_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should show password reset form" do
|
||||
get new_password_reset_url
|
||||
assert_select "form"
|
||||
assert_select "input[type=email]"
|
||||
end
|
||||
|
||||
# CREATE tests
|
||||
test "should send reset email for existing user" do
|
||||
assert_enqueued_emails 1 do
|
||||
post password_resets_url, params: { email: @user.email }
|
||||
end
|
||||
assert_redirected_to login_url
|
||||
assert_match /password reset instructions/i, flash[:notice]
|
||||
end
|
||||
|
||||
test "should generate reset token for existing user" do
|
||||
post password_resets_url, params: { email: @user.email }
|
||||
@user.reload
|
||||
assert_not_nil @user.reset_password_token
|
||||
assert_not_nil @user.reset_password_sent_at
|
||||
end
|
||||
|
||||
test "should handle non-existent email gracefully" do
|
||||
post password_resets_url, params: { email: "nonexistent@example.com" }
|
||||
assert_redirected_to login_url
|
||||
assert_match /if that email address is in our system/i, flash[:notice]
|
||||
end
|
||||
|
||||
test "should send invitation for user without accepted invitation" do
|
||||
pending_user = users(:pending_invitation)
|
||||
assert_nil pending_user.invitation_accepted_at
|
||||
|
||||
assert_enqueued_emails 1 do
|
||||
post password_resets_url, params: { email: pending_user.email }
|
||||
end
|
||||
assert_redirected_to login_url
|
||||
end
|
||||
|
||||
test "should handle blank email" do
|
||||
post password_resets_url, params: { email: "" }
|
||||
assert_redirected_to login_url
|
||||
end
|
||||
|
||||
test "should handle email with whitespace" do
|
||||
post password_resets_url, params: { email: " #{@user.email} " }
|
||||
@user.reload
|
||||
assert_not_nil @user.reset_password_token
|
||||
end
|
||||
|
||||
# EDIT tests
|
||||
test "should show reset password form with valid token" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
get edit_password_reset_url(@user.reset_password_token)
|
||||
assert_response :success
|
||||
assert_select "form"
|
||||
assert_select "input[type=password]", count: 2
|
||||
end
|
||||
|
||||
test "should reject invalid token" do
|
||||
get edit_password_reset_url("invalid_token")
|
||||
assert_redirected_to login_url
|
||||
assert_match /invalid/i, flash[:alert]
|
||||
end
|
||||
|
||||
test "should reject expired token" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: 2.hours.ago
|
||||
)
|
||||
get edit_password_reset_url(@user.reset_password_token)
|
||||
assert_redirected_to new_password_reset_url
|
||||
assert_match /expired/i, flash[:alert]
|
||||
end
|
||||
|
||||
# UPDATE tests
|
||||
test "should reset password with valid token" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "newpassword12345",
|
||||
password_confirmation: "newpassword12345"
|
||||
}
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert_match /password has been reset/i, flash[:notice]
|
||||
|
||||
@user.reload
|
||||
assert_nil @user.reset_password_token
|
||||
assert_nil @user.reset_password_sent_at
|
||||
assert @user.authenticate("newpassword12345")
|
||||
end
|
||||
|
||||
test "should auto-login after successful password reset" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "newpassword12345",
|
||||
password_confirmation: "newpassword12345"
|
||||
}
|
||||
|
||||
assert_equal @user.id, session[:user_id]
|
||||
end
|
||||
|
||||
test "should reject mismatched passwords" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "newpassword12345",
|
||||
password_confirmation: "differentpassword"
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_match /doesn't match/i, flash[:alert]
|
||||
end
|
||||
|
||||
test "should reject blank password" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "",
|
||||
password_confirmation: ""
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_match /cannot be blank/i, flash[:alert]
|
||||
end
|
||||
|
||||
test "should reject expired token on update" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: 2.hours.ago
|
||||
)
|
||||
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "newpassword12345",
|
||||
password_confirmation: "newpassword12345"
|
||||
}
|
||||
|
||||
assert_redirected_to new_password_reset_url
|
||||
assert_match /expired/i, flash[:alert]
|
||||
end
|
||||
|
||||
test "should reject invalid token on update" do
|
||||
patch password_reset_url("invalid_token"), params: {
|
||||
password: "newpassword12345",
|
||||
password_confirmation: "newpassword12345"
|
||||
}
|
||||
|
||||
assert_redirected_to login_url
|
||||
assert_match /invalid/i, flash[:alert]
|
||||
end
|
||||
|
||||
test "should enforce password validations" do
|
||||
@user.update!(
|
||||
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||
reset_password_sent_at: Time.current
|
||||
)
|
||||
|
||||
# Password too short (less than 12 characters)
|
||||
patch password_reset_url(@user.reset_password_token), params: {
|
||||
password: "short",
|
||||
password_confirmation: "short"
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user