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 assert_select "form" assert_select "input[name='user[email]']" assert_select "input[name='user[name]']" assert_select "select[name='user[role]']" assert_select "select[name='user[primary_language]']" end test "should display pending invitations on index page" do login_as(users(:admin_user)) get admin_invitations_path assert_response :success assert_select "h1,h2", /Invitations/ 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 send invitation email when creating invitation" do login_as(users(:admin_user)) assert_enqueued_emails 1 do post admin_invitations_path, params: { user: { email: "newuser@example.com", name: "New User", role: "contributor", primary_language: "en" } } end 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 # 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