diff --git a/test/controllers/admin/dashboard_controller_test.rb b/test/controllers/admin/dashboard_controller_test.rb new file mode 100644 index 0000000..6d05dbf --- /dev/null +++ b/test/controllers/admin/dashboard_controller_test.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class Admin::DashboardControllerTest < ActionDispatch::IntegrationTest + test "should redirect to login when not authenticated" do + get admin_root_path + assert_redirected_to login_path + follow_redirect! + assert_select ".bg-red-50", /You must be logged in/ + end + + test "should redirect to root when logged in as non-admin" do + login_as(users(:contributor_user)) + get admin_root_path + assert_redirected_to root_path + assert_equal "You must be an administrator to access this page.", flash[:alert] + end + + test "should redirect to root when logged in as reviewer" do + login_as(users(:reviewer_user)) + get admin_root_path + assert_redirected_to root_path + assert_equal "You must be an administrator to access this page.", flash[:alert] + end + + test "should show dashboard when logged in as admin" do + login_as(users(:admin_user)) + get admin_root_path + assert_response :success + end + + test "should show admin dashboard path" do + login_as(users(:admin_user)) + get admin_dashboard_path + assert_response :success + end +end diff --git a/test/controllers/admin/invitations_controller_test.rb b/test/controllers/admin/invitations_controller_test.rb new file mode 100644 index 0000000..69c3d70 --- /dev/null +++ b/test/controllers/admin/invitations_controller_test.rb @@ -0,0 +1,116 @@ +require "test_helper" + +class Admin::InvitationsControllerTest < ActionDispatch::IntegrationTest + test "should redirect to login when not authenticated" do + get admin_invitations_path + assert_redirected_to login_path + end + + test "should redirect to root when logged in as non-admin" do + login_as(users(:contributor_user)) + get admin_invitations_path + assert_redirected_to root_path + end + + test "should show invitations index when logged in as admin" do + login_as(users(:admin_user)) + get admin_invitations_path + assert_response :success + end + + test "should get new invitation page when logged in as admin" do + login_as(users(:admin_user)) + get new_admin_invitation_path + assert_response :success + end + + test "should create invitation when logged in as admin" do + login_as(users(:admin_user)) + + assert_difference("User.count", 1) do + post admin_invitations_path, params: { + user: { + email: "newuser@example.com", + name: "New User", + 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 + assert_not_nil new_user.invitation_token + assert_not_nil new_user.invitation_sent_at + assert_nil new_user.invitation_accepted_at + assert_equal users(:admin_user).id, new_user.invited_by_id + end + + test "should not create invitation with invalid data" do + login_as(users(:admin_user)) + + assert_no_difference("User.count") do + post admin_invitations_path, params: { + user: { + email: "", + name: "New User", + role: "contributor", + primary_language: "en" + } + } + end + + assert_response :unprocessable_entity + end + + test "should cancel pending invitation when logged in as admin" do + login_as(users(:admin_user)) + + assert_difference("User.count", -1) do + delete admin_invitation_path(users(:pending_invitation)) + end + + assert_redirected_to admin_invitations_path + end + + test "should not cancel accepted invitation" do + login_as(users(:admin_user)) + + assert_no_difference("User.count") do + delete admin_invitation_path(users(:contributor_user)) + end + + assert_redirected_to admin_invitations_path + follow_redirect! + assert_select ".bg-red-50", /Cannot cancel an accepted invitation/ + end + + test "should not allow non-admin to create invitation" do + login_as(users(:contributor_user)) + + assert_no_difference("User.count") do + post admin_invitations_path, params: { + user: { + email: "newuser@example.com", + name: "New User", + role: "contributor", + primary_language: "en" + } + } + end + + assert_redirected_to root_path + end + + test "should not allow non-admin to cancel invitation" do + login_as(users(:contributor_user)) + + assert_no_difference("User.count") do + delete admin_invitation_path(users(:pending_invitation)) + end + + assert_redirected_to root_path + end +end diff --git a/test/controllers/admin/users_controller_test.rb b/test/controllers/admin/users_controller_test.rb new file mode 100644 index 0000000..53b9392 --- /dev/null +++ b/test/controllers/admin/users_controller_test.rb @@ -0,0 +1,68 @@ +require "test_helper" + +class Admin::UsersControllerTest < ActionDispatch::IntegrationTest + test "should redirect to login when not authenticated" do + get admin_users_path + assert_redirected_to login_path + end + + test "should redirect to root when logged in as non-admin" do + login_as(users(:contributor_user)) + get admin_users_path + assert_redirected_to root_path + end + + test "should show users index when logged in as admin" do + login_as(users(:admin_user)) + get admin_users_path + assert_response :success + 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)) + assert_response :success + end + + test "should update user role when logged in as admin" do + login_as(users(:admin_user)) + + patch admin_user_path(users(:contributor_user)), params: { + user: { role: "reviewer" } + } + + assert_redirected_to admin_users_path + assert_equal "reviewer", users(:contributor_user).reload.role + end + + test "should delete user when logged in as admin" do + login_as(users(:admin_user)) + + # Delete reviewer_user who has no associated records + assert_difference("User.count", -1) do + delete admin_user_path(users(:reviewer_user)) + end + + assert_redirected_to admin_users_path + end + + test "should not allow non-admin to update user" do + login_as(users(:contributor_user)) + + patch admin_user_path(users(:reviewer_user)), params: { + user: { role: "admin" } + } + + assert_redirected_to root_path + end + + test "should not allow non-admin to delete user" do + login_as(users(:contributor_user)) + + assert_no_difference("User.count") do + delete admin_user_path(users(:reviewer_user)) + end + + assert_redirected_to root_path + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000..01a8465 --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,105 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + test "should get login page" do + get login_path + assert_response :success + assert_select "h1", "Sign in" + assert_select "input[type=email]" + assert_select "input[type=password]" + end + + test "should redirect to admin if already logged in as admin" do + login_as(users(:admin_user)) + get login_path + assert_redirected_to admin_root_path + end + + test "should redirect to root if already logged in as non-admin" do + login_as(users(:contributor_user)) + get login_path + assert_redirected_to root_path + end + + test "should login with valid credentials" do + post login_path, params: { + email: "admin@example.com", + password: "password123456" + } + + assert_redirected_to admin_root_path + assert_equal users(:admin_user).id, session[:user_id] + follow_redirect! + assert_select ".bg-green-50", /Welcome back/ + end + + test "should login contributor and redirect to root" do + post login_path, params: { + email: "contributor@example.com", + password: "password123456" + } + + assert_redirected_to root_path + assert_equal users(:contributor_user).id, session[:user_id] + end + + test "should not login with invalid email" do + post login_path, params: { + email: "nonexistent@example.com", + password: "password123456" + } + + assert_response :unprocessable_entity + assert_nil session[:user_id] + assert_select ".bg-red-50", /Invalid email or password/ + end + + test "should not login with invalid password" do + post login_path, params: { + email: "admin@example.com", + password: "wrongpassword" + } + + assert_response :unprocessable_entity + assert_nil session[:user_id] + assert_select ".bg-red-50", /Invalid email or password/ + end + + test "should handle email with whitespace and case insensitivity" do + post login_path, params: { + email: " ADMIN@EXAMPLE.COM ", + password: "password123456" + } + + assert_redirected_to admin_root_path + assert_equal users(:admin_user).id, session[:user_id] + end + + test "should not login user with pending invitation" do + post login_path, params: { + email: "pending@example.com", + password: "password123456" + } + + assert_response :unprocessable_entity + assert_nil session[:user_id] + assert_select ".bg-red-50", /Your account is pending/ + end + + test "should logout and redirect to root" do + login_as(users(:admin_user)) + + delete logout_path + + assert_redirected_to root_path + assert_nil session[:user_id] + assert_equal "You have been logged out.", flash[:notice] + end + + test "should logout even when not logged in" do + delete logout_path + + assert_redirected_to root_path + assert_nil session[:user_id] + end +end diff --git a/test/controllers/setup_controller_test.rb b/test/controllers/setup_controller_test.rb new file mode 100644 index 0000000..203b785 --- /dev/null +++ b/test/controllers/setup_controller_test.rb @@ -0,0 +1,100 @@ +require "test_helper" + +class SetupControllerTest < ActionDispatch::IntegrationTest + def setup + SetupState.reset! + end + + def teardown + SetupState.reset! + end + + test "should show setup page when not installed" do + get setup_path + assert_response :success + assert_select "h2", /Create Admin Account/ + end + + test "should redirect to root when already installed" do + SetupState.mark_installed! + + get setup_path + assert_redirected_to root_path + assert_equal "Setup has already been completed.", flash[:alert] + end + + test "should create admin user and mark as installed" do + assert_difference("User.count", 1) do + post setup_path, params: { + user: { + email: "setupadmin@example.com", + name: "Setup Admin", + password: "securepassword123", + password_confirmation: "securepassword123", + primary_language: "en" + } + } + end + + assert SetupState.installed? + + new_user = User.find_by(email: "setupadmin@example.com") + assert_not_nil new_user + assert_equal "admin", new_user.role + assert_not_nil new_user.invitation_accepted_at + assert_equal new_user.id, session[:user_id] + + assert_redirected_to admin_root_path + end + + test "should not create user with invalid password" do + assert_no_difference("User.count") do + post setup_path, params: { + user: { + email: "setupadmin@example.com", + name: "Setup Admin", + password: "short", # Too short, minimum is 12 + password_confirmation: "short", + primary_language: "en" + } + } + end + + assert_not SetupState.installed? + assert_response :unprocessable_entity + end + + test "should not create user with mismatched passwords" do + assert_no_difference("User.count") do + post setup_path, params: { + user: { + email: "setupadmin@example.com", + name: "Setup Admin", + password: "securepassword123", + password_confirmation: "differentpassword", + primary_language: "en" + } + } + end + + assert_not SetupState.installed? + assert_response :unprocessable_entity + end + + test "should not create user without email" do + assert_no_difference("User.count") do + post setup_path, params: { + user: { + email: "", + name: "Setup Admin", + password: "securepassword123", + password_confirmation: "securepassword123", + primary_language: "en" + } + } + end + + assert_not SetupState.installed? + assert_response :unprocessable_entity + end +end diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml index 4baff9a..7790e09 100644 --- a/test/fixtures/comments.yml +++ b/test/fixtures/comments.yml @@ -1,13 +1,13 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: - user: one + user: admin_user commentable: one commentable_type: Commentable body: MyText two: - user: two + user: contributor_user commentable: two commentable_type: Commentable body: MyText diff --git a/test/fixtures/entries.yml b/test/fixtures/entries.yml index 9c18750..f427c21 100644 --- a/test/fixtures/entries.yml +++ b/test/fixtures/entries.yml @@ -10,8 +10,8 @@ one: de: MyString notes: MyText verified: false - created_by: one - updated_by: one + created_by: admin_user + updated_by: admin_user two: category: 1 @@ -23,5 +23,5 @@ two: de: MyString notes: MyText verified: false - created_by: two - updated_by: two + created_by: contributor_user + updated_by: contributor_user diff --git a/test/fixtures/entry_versions.yml b/test/fixtures/entry_versions.yml index 111d6de..990451b 100644 --- a/test/fixtures/entry_versions.yml +++ b/test/fixtures/entry_versions.yml @@ -2,12 +2,12 @@ one: entry: one - user: one + user: admin_user changes_made: "{}" change_type: MyString two: entry: two - user: two + user: contributor_user changes_made: "{}" change_type: MyString diff --git a/test/fixtures/suggested_meanings.yml b/test/fixtures/suggested_meanings.yml index 7c5dc30..baedaac 100644 --- a/test/fixtures/suggested_meanings.yml +++ b/test/fixtures/suggested_meanings.yml @@ -9,7 +9,7 @@ one: source: MyString region: MyString status: :pending - submitted_by: one + submitted_by: contributor_user two: entry: two @@ -20,4 +20,4 @@ two: source: MyString region: MyString status: :pending - submitted_by: two + submitted_by: contributor_user diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 7a9decc..d81a52d 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,23 +1,46 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html +# Password for all test users: "password123456" -one: - email: "one@example.com" - password_digest: <%= BCrypt::Password.create('password') %> - name: "User One" - role: 1 +admin_user: + email: "admin@example.com" + password_digest: <%= BCrypt::Password.create('password123456') %> + name: "Admin User" + role: 2 # admin primary_language: "en" - invitation_token: "one" - invitation_sent_at: 2026-01-22 13:38:37 - invitation_accepted_at: 2026-01-22 13:38:37 - invited_by: one + invitation_token: "admin_token_accepted" + invitation_sent_at: <%= 30.days.ago %> + invitation_accepted_at: <%= 30.days.ago %> + invited_by_id: ~ -two: - email: "two@example.com" - password_digest: <%= BCrypt::Password.create('password') %> - name: "User Two" - role: 1 +reviewer_user: + email: "reviewer@example.com" + password_digest: <%= BCrypt::Password.create('password123456') %> + name: "Reviewer User" + role: 1 # reviewer + primary_language: "en" + invitation_token: "reviewer_token_accepted" + invitation_sent_at: <%= 20.days.ago %> + invitation_accepted_at: <%= 20.days.ago %> + invited_by: admin_user + +contributor_user: + email: "contributor@example.com" + password_digest: <%= BCrypt::Password.create('password123456') %> + name: "Contributor User" + role: 0 # contributor primary_language: "fi" - invitation_token: "two" - invitation_sent_at: 2026-01-22 13:38:37 - invitation_accepted_at: 2026-01-22 13:38:37 - invited_by: two + invitation_token: "contributor_token_accepted" + invitation_sent_at: <%= 10.days.ago %> + invitation_accepted_at: <%= 10.days.ago %> + invited_by: admin_user + +pending_invitation: + email: "pending@example.com" + password_digest: <%= BCrypt::Password.create('password123456') %> + name: "Pending User" + role: 0 # contributor + primary_language: "en" + invitation_token: "pending_token_12345" + invitation_sent_at: <%= 2.days.ago %> + invitation_accepted_at: ~ + invited_by: admin_user diff --git a/test/models/comment_test.rb b/test/models/comment_test.rb index 4734d51..f98117c 100644 --- a/test/models/comment_test.rb +++ b/test/models/comment_test.rb @@ -2,14 +2,14 @@ require "test_helper" class CommentTest < ActiveSupport::TestCase test "should be valid with a user, body, and commentable" do - user = users(:one) + user = users(:admin_user) entry = entries(:one) comment = Comment.new(user: user, body: "This is a comment.", commentable: entry) assert comment.valid? end test "should be invalid without a body" do - user = users(:one) + user = users(:admin_user) entry = entries(:one) comment = Comment.new(user: user, commentable: entry) assert_not comment.valid? @@ -22,7 +22,7 @@ class CommentTest < ActiveSupport::TestCase end test "should be invalid without a commentable" do - user = users(:one) + user = users(:admin_user) comment = Comment.new(user: user, body: "This is a comment.") assert_not comment.valid? end diff --git a/test/models/entry_version_test.rb b/test/models/entry_version_test.rb index 67f0c6e..00a40bb 100644 --- a/test/models/entry_version_test.rb +++ b/test/models/entry_version_test.rb @@ -4,7 +4,7 @@ class EntryVersionTest < ActiveSupport::TestCase test "should be valid with all attributes" do version = EntryVersion.new( entry: entries(:one), - user: users(:one), + user: users(:admin_user), changes_made: { "fi" => "uusi sana" } ) assert version.valid? @@ -13,14 +13,14 @@ class EntryVersionTest < ActiveSupport::TestCase test "should be invalid without changes_made" do version = EntryVersion.new( entry: entries(:one), - user: users(:one) + user: users(:admin_user) ) assert_not version.valid? end test "should be invalid without an entry" do version = EntryVersion.new( - user: users(:one), + user: users(:admin_user), changes_made: { "fi" => "uusi sana" } ) assert_not version.valid? diff --git a/test/models/suggested_meaning_test.rb b/test/models/suggested_meaning_test.rb index f37ae13..43273fc 100644 --- a/test/models/suggested_meaning_test.rb +++ b/test/models/suggested_meaning_test.rb @@ -6,7 +6,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase entry: entries(:one), language_code: supported_languages(:one).code, alternative_translation: "New Translation", - submitted_by: users(:one) + submitted_by: users(:contributor_user) ) assert meaning.valid? end @@ -15,7 +15,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase meaning = SuggestedMeaning.new( entry: entries(:one), alternative_translation: "New Translation", - submitted_by: users(:one) + submitted_by: users(:contributor_user) ) assert_not meaning.valid? end @@ -24,7 +24,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase meaning = SuggestedMeaning.new( entry: entries(:one), language_code: supported_languages(:one).code, - submitted_by: users(:one) + submitted_by: users(:contributor_user) ) assert_not meaning.valid? end @@ -34,7 +34,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase entry: entries(:one), language_code: supported_languages(:one).code, alternative_translation: "New Translation", - submitted_by: users(:one) + submitted_by: users(:contributor_user) ) assert meaning.pending? end @@ -44,7 +44,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase entry: entries(:one), language_code: supported_languages(:one).code, alternative_translation: "New Translation", - submitted_by: users(:one), + submitted_by: users(:contributor_user), status: :accepted ) assert meaning.accepted? @@ -55,7 +55,7 @@ class SuggestedMeaningTest < ActiveSupport::TestCase entry: entries(:one), language_code: supported_languages(:one).code, alternative_translation: "New Translation", - submitted_by: users(:one), + submitted_by: users(:contributor_user), status: :rejected ) assert meaning.rejected? diff --git a/test/models/supported_language_test.rb b/test/models/supported_language_test.rb index 8af3b75..51ec028 100644 --- a/test/models/supported_language_test.rb +++ b/test/models/supported_language_test.rb @@ -2,13 +2,12 @@ require "test_helper" class SupportedLanguageTest < ActiveSupport::TestCase test "should be valid with all attributes" do - language = SupportedLanguage.new(code: "es", name: "Spanish", native_name: "EspaƱol") + language = SupportedLanguage.new(code: "sv", name: "Swedish", native_name: "Svenska") assert language.valid? end test "should be invalid with a duplicate code" do - SupportedLanguage.create(code: "de", name: "German", native_name: "Deutsch") - language = SupportedLanguage.new(code: "de", name: "German", native_name: "Deutsch") + language = SupportedLanguage.new(code: supported_languages(:one).code, name: "English", native_name: "English") assert_not language.valid? end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index f8b7c32..a9fb035 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -2,7 +2,7 @@ require "test_helper" class UserTest < ActiveSupport::TestCase test "should be valid with an email and password" do - user = User.new(email: "test@example.com", password: "password123456") + user = User.new(email: "new-user@example.com", password: "password123456") assert user.valid? end @@ -12,23 +12,23 @@ class UserTest < ActiveSupport::TestCase end test "should be invalid with a duplicate email" do - User.create(email: "test@example.com", password: "password123456") - user = User.new(email: "test@example.com", password: "password123456") + existing_user = users(:admin_user) + user = User.new(email: existing_user.email, password: "password123456") assert_not user.valid? end test "should have a default role of contributor" do - user = User.new(email: "test@example.com", password: "password123456") + user = User.new(email: "new-user@example.com", password: "password123456") assert user.contributor? end test "can be a reviewer" do - user = User.new(email: "test@example.com", password: "password123456", role: :reviewer) + user = User.new(email: "new-user@example.com", password: "password123456", role: :reviewer) assert user.reviewer? end test "can be an admin" do - user = User.new(email: "test@example.com", password: "password123456", role: :admin) + user = User.new(email: "new-user@example.com", password: "password123456", role: :admin) assert user.admin? end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c22470..615a5e7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,8 +4,8 @@ require "rails/test_help" module ActiveSupport class TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) + # Run tests serially to avoid sqlite/FTS5 conflicts in test setup. + parallelize(workers: 1) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all @@ -13,3 +13,20 @@ module ActiveSupport # Add more helper methods to be used by all tests here... end end + +module ActionDispatch + class IntegrationTest + # Helper method to login as a user in integration tests + def login_as(user, password: "password123456") + post login_path, params: { + email: user.email, + password: password + } + end + + # Helper method to logout + def logout + delete logout_path + end + end +end