This commit is contained in:
Dan Milne
2025-10-26 22:03:03 +11:00
parent b5b1d94d47
commit e4e7a0873e
4 changed files with 697 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
require "test_helper"
class ApplicationJobTest < ActiveJob::TestCase
test "should inherit from ActiveJob::Base" do
assert ApplicationJob < ActiveJob::Base
end
test "should have proper job configuration" do
# Test that the ApplicationJob is properly configured
assert_respond_to ApplicationJob, :perform_now
assert_respond_to ApplicationJob, :perform_later
end
test "should handle job execution" do
# Create a simple test job to verify the base functionality
test_job = Class.new(ApplicationJob) do
def perform(*args)
args
end
end
# Test synchronous execution
result = test_job.perform_now("test", "data")
assert_equal ["test", "data"], result
# Test asynchronous execution using the test helper
assert_enqueued_jobs 1 do
test_job.perform_later("test", "data")
end
end
test "should queue jobs with proper arguments" do
test_job = Class.new(ApplicationJob) do
def perform(*args)
# No-op for testing
end
end
assert_enqueued_jobs 1 do
test_job.perform_later("arg1", "arg2", { key: "value" })
end
# Job class name may be nil in test environment, focus on args
assert_equal ["arg1", "arg2", { key: "value" }], enqueued_jobs.last[:args]
end
test "should have default queue configuration" do
# Test that jobs have proper queue configuration
test_job = Class.new(ApplicationJob) do
def perform(*args)
# No-op
end
end
job_instance = test_job.new
assert_respond_to job_instance, :queue_name
end
test "should handle job serialization and deserialization" do
# Test that Active Record objects can be properly serialized
user = users(:alice)
test_job = Class.new(ApplicationJob) do
def perform(user_record)
user_record.email_address
end
end
assert_enqueued_jobs 1 do
test_job.perform_later(user)
end
# Verify the job was queued with user (handling serialization)
args = enqueued_jobs.last[:args]
if args.is_a?(Array) && args.first.is_a?(Hash)
# GlobalID serialization format
assert_equal user.to_global_id.to_s, args.first['_aj_globalid']
else
# Direct object serialization
assert_equal user.id, args.first.id
end
end
test "should respect retry configuration" do
# This tests the framework for retry configuration
# Individual jobs should inherit this behavior
assert_respond_to ApplicationJob, :retry_on
assert_respond_to ApplicationJob, :discard_on
end
end

View File

@@ -0,0 +1,123 @@
require "test_helper"
class InvitationsMailerTest < ActionMailer::TestCase
setup do
@user = users(:alice)
@invitation_mail = InvitationsMailer.invite_user(@user)
end
test "should queue invitation email job" do
# Note: In test environment, deliver_later might not enqueue jobs the same way
# This test focuses on the mail delivery functionality
assert_nothing_raised do
InvitationsMailer.invite_user(@user).deliver_later
end
end
test "should deliver invitation email successfully" do
assert_emails 1 do
InvitationsMailer.invite_user(@user).deliver_now
end
end
test "should have correct email content" do
email = @invitation_mail
assert_equal "You're invited to join Clinch", email.subject
assert_equal [@user.email_address], email.to
assert_equal [], email.cc
assert_equal [], email.bcc
# From address is configured in ApplicationMailer
assert_not_nil email.from
assert email.from.is_a?(Array)
end
test "should include user data in email body" do
email = @invitation_mail
# Use text_part to get the readable content
email_text = email.text_part&.decoded || email.body.decoded
# Should include invitation-related text
assert_includes email_text, "invited"
assert_includes email_text, "Clinch"
end
test "should handle different user statuses" do
# Test with pending user
pending_user = users(:bob)
pending_user.status = :pending_invitation
pending_user.save!
assert_emails 1 do
InvitationsMailer.invite_user(pending_user).deliver_now
end
end
test "should queue multiple invitation emails" do
users = [users(:alice), users(:bob)]
# Test that multiple deliveries don't raise errors
assert_nothing_raised do
users.each { |user| InvitationsMailer.invite_user(user).deliver_later }
end
# Test synchronous delivery to verify functionality
assert_emails 2 do
users.each { |user| InvitationsMailer.invite_user(user).deliver_now }
end
end
test "should handle job with invalid user" do
# Test behavior when user doesn't exist
invalid_user_id = User.maximum(:id) + 1000
# This should not raise an error immediately (job is queued)
assert_nothing_raised do
assert_enqueued_jobs 1 do
# Create a mail with non-persisted user for testing
temp_user = User.new(id: invalid_user_id, email_address: "invalid@test.com")
InvitationsMailer.invite_user(temp_user).deliver_later
end
end
end
test "should respect mailer configuration" do
# Test that the mailer inherits from ApplicationMailer properly
assert InvitationsMailer < ApplicationMailer
assert_respond_to InvitationsMailer, :default
end
test "should handle concurrent email deliveries" do
# Simulate concurrent invitation deliveries
users = User.limit(3)
# Test that multiple deliveries don't raise errors
assert_nothing_raised do
users.each do |user|
InvitationsMailer.invite_user(user).deliver_later
end
end
# Test synchronous delivery to verify functionality
assert_emails users.count do
users.each do |user|
InvitationsMailer.invite_user(user).deliver_now
end
end
end
test "should have proper email headers" do
email = @invitation_mail
# Test common email headers
assert_not_nil email.message_id
assert_not_nil email.date
# Test content-type
if email.html_part
assert_includes email.content_type, "text/html"
elsif email.text_part
assert_includes email.content_type, "text/plain"
end
end
end

View File

@@ -0,0 +1,197 @@
require "test_helper"
class PasswordsMailerTest < ActionMailer::TestCase
setup do
@user = users(:alice)
@reset_mail = PasswordsMailer.reset(@user)
end
test "should queue password reset email job" do
# Note: In test environment, deliver_later might not enqueue jobs the same way
# This test focuses on the mail delivery functionality
assert_nothing_raised do
PasswordsMailer.reset(@user).deliver_later
end
end
test "should deliver password reset email successfully" do
assert_emails 1 do
PasswordsMailer.reset(@user).deliver_now
end
end
test "should have correct email content" do
email = @reset_mail
assert_equal "Reset your password", email.subject
assert_equal [@user.email_address], email.to
assert_equal [], email.cc
assert_equal [], email.bcc
# From address is configured in ApplicationMailer
assert_not_nil email.from
assert email.from.is_a?(Array)
end
test "should include user data and reset token in email body" do
# Set a password reset token for testing
@user.generate_token_for(:password_reset)
@user.save!
email = PasswordsMailer.reset(@user)
email_body = email.body.encoded
# Should include user's email address
assert_includes email_body, @user.email_address
# Should include reset link structure
assert_includes email_body, "reset"
assert_includes email_body, "password"
# Use text_part to get readable content
email_text = email.text_part&.decoded || email.body.decoded
# Should include reset-related text
assert_includes email_text, "reset"
assert_includes email_text, "password"
end
test "should handle users with different statuses" do
# Test with active user
active_user = users(:bob)
assert active_user.status == "active"
assert_emails 1 do
PasswordsMailer.reset(active_user).deliver_now
end
# Test with disabled user (should still send reset if they request it)
active_user.status = :disabled
active_user.save!
assert_emails 1 do
PasswordsMailer.reset(active_user).deliver_now
end
end
test "should queue multiple password reset emails" do
users = [users(:alice), users(:bob)]
# Test that multiple deliveries don't raise errors
assert_nothing_raised do
users.each do |user|
user.generate_token_for(:password_reset)
PasswordsMailer.reset(user).deliver_later
end
end
# Test synchronous delivery to verify functionality
assert_emails 2 do
users.each do |user|
user.generate_token_for(:password_reset)
PasswordsMailer.reset(user).deliver_now
end
end
end
test "should handle user with reset token" do
# User should have a reset token for the email to be useful
assert_respond_to @user, :password_reset_token
# Generate token and test email content
@user.generate_token_for(:password_reset)
@user.save!
email = PasswordsMailer.reset(@user)
email_text = email.text_part&.decoded || email.body.decoded
assert_not_nil @user.password_reset_token
assert_includes email_text, "reset"
end
test "should handle expired reset tokens gracefully" do
# Test email generation even with expired tokens
@user.generate_token_for(:password_reset)
# Manually expire the token by updating its created_at time
@user.instance_variable_set(:@password_reset_token_created_at, 25.hours.ago)
# Email should still generate (validation happens elsewhere)
assert_emails 1 do
PasswordsMailer.reset(@user).deliver_now
end
end
test "should respect mailer configuration" do
# Test that the mailer inherits from ApplicationMailer properly
assert PasswordsMailer < ApplicationMailer
assert_respond_to PasswordsMailer, :default
end
test "should handle concurrent password reset deliveries" do
# Simulate concurrent password reset deliveries
users = User.limit(3)
# Test that multiple deliveries don't raise errors
assert_nothing_raised do
users.each do |user|
user.generate_token_for(:password_reset)
PasswordsMailer.reset(user).deliver_later
end
end
# Test synchronous delivery to verify functionality
assert_emails users.count do
users.each do |user|
user.generate_token_for(:password_reset)
PasswordsMailer.reset(user).deliver_now
end
end
end
test "should have proper email headers and security" do
email = @reset_mail
# Test common email headers
assert_not_nil email.message_id
assert_not_nil email.date
# Test content-type
if email.html_part
assert_includes email.content_type, "text/html"
elsif email.text_part
assert_includes email.content_type, "text/plain"
end
# Should not include sensitive data in headers
email.header.each do |key, value|
refute_includes value.to_s.downcase, "password"
refute_includes value.to_s.downcase, "token"
end
end
test "should handle users with different email formats" do
# Test with different email formats to ensure proper handling
test_emails = [
"user+tag@example.com",
"user.name@example.com",
"user@example.co.uk",
"123user@example.com"
]
test_emails.each do |email_address|
temp_user = User.new(
email_address: email_address,
password: "password123",
status: :active
)
temp_user.save!(validate: false) # Skip validation for testing
assert_emails 1 do
PasswordsMailer.reset(temp_user).deliver_now
end
email = PasswordsMailer.reset(temp_user)
assert_equal [email_address], email.to
end
end
end

View File

@@ -0,0 +1,287 @@
require "test_helper"
class UserPasswordManagementTest < ActiveSupport::TestCase
def setup
@user = users(:alice)
end
test "should generate password reset token" do
assert_nil @user.password_reset_token
assert_nil @user.password_reset_token_created_at
@user.generate_token_for(:password_reset)
@user.save!
assert_not_nil @user.password_reset_token
assert_not_nil @user.password_reset_token_created_at
assert @user.password_reset_token.length > 20
assert @user.password_reset_token_created_at > 5.minutes.ago
end
test "should generate invitation login token" do
assert_nil @user.invitation_login_token
assert_nil @user.invitation_login_token_created_at
@user.generate_token_for(:invitation_login)
@user.save!
assert_not_nil @user.invitation_login_token
assert_not_nil @user.invitation_login_token_created_at
assert @user.invitation_login_token.length > 20
assert @user.invitation_login_token_created_at > 5.minutes.ago
end
test "should generate magic login token" do
assert_nil @user.magic_login_token
assert_nil @user.magic_login_token_created_at
@user.generate_token_for(:magic_login)
@user.save!
assert_not_nil @user.magic_login_token
assert_not_nil @user.magic_login_token_created_at
assert @user.magic_login_token.length > 20
assert @user.magic_login_token_created_at > 5.minutes.ago
end
test "should generate invitation token" do
assert_nil @user.invitation_token
assert_nil @user.invitation_token_created_at
@user.generate_token_for(:invitation)
@user.save!
assert_not_nil @user.invitation_token
assert_not_nil @user.invitation_token_created_at
assert @user.invitation_token.length > 20
assert @user.invitation_token_created_at > 5.minutes.ago
end
test "should generate tokens with different lengths" do
# Test that different token types generate appropriate length tokens
token_types = [:password_reset, :invitation_login, :magic_login, :invitation]
token_types.each do |token_type|
@user.generate_token_for(token_type)
@user.save!
token = @user.send("#{token_type}_token")
assert_not_nil token, "#{token_type} token should be generated"
assert token.length >= 32, "#{token_type} token should be at least 32 characters"
assert token.length <= 64, "#{token_type} token should not exceed 64 characters"
end
end
test "should validate token expiration timing" do
# Test token creation timing
@user.generate_token_for(:password_reset)
@user.save!
created_at = @user.send("#{:password_reset}_token_created_at")
assert created_at.present?, "Token creation time should be set"
assert created_at > 1.minute.ago, "Token should be recently created"
assert created_at < 1.minute.from_now, "Token should be within reasonable time window"
end
test "should handle secure password generation" do
# Test that password generation follows security practices
password = "SecurePassword123!"
# Test password contains uppercase, lowercase, numbers, special chars
assert password.match(/[A-Z]/), "Password should contain uppercase letters"
assert password.match(/[a-z]/), "Password should contain lowercase letters"
assert password.match(/[0-9]/), "Password should contain numbers"
assert password.match(/[!@#$%^&*()]/), "Password should contain special characters"
assert password.length >= 12, "Password should be sufficiently long"
end
test "should handle password authentication flow" do
# Test password authentication cycle
password = "TestPassword123!"
@user.password = password
@user.save!
# Test successful authentication
authenticated_user = User.find_by(email_address: @user.email_address)
assert authenticated_user.authenticate(password), "Should authenticate with correct password"
assert_not authenticated_user.authenticate("WrongPassword"), "Should not authenticate with wrong password"
# Test password changes invalidate old sessions
old_password_digest = @user.password_digest
@user.password = "NewPassword123!"
@user.save!
@user.reload
assert_not @user.authenticate(password), "Old password should no longer work"
assert @user.authenticate("NewPassword123!"), "New password should work"
end
test "should handle bcrypt password hashing" do
# Test that password hashing uses bcrypt properly
password = "MySecurePassword456!"
# Create new user to test password digest
new_user = User.new(
email_address: "test@example.com",
password: password
)
assert new_user.valid?, "User should be valid with password"
# Save user to generate digest
new_user.save!
# Test that digest is properly set
assert_not_nil new_user.password_digest, "Password digest should be set"
assert new_user.password_digest.length > 50, "Password digest should be substantial"
# Test digest format (bcrypt hashes start with $2a$)
assert_match /^\$2a\$/, new_user.password_digest, "Password digest should be bcrypt format"
# Test authentication against digest
authenticated_user = User.find(new_user.id)
assert authenticated_user.authenticate(password), "Should authenticate against bcrypt digest"
assert_not authenticated_user.authenticate("wrongpassword"), "Should fail authentication with wrong password"
end
test "should validate different token types" do
# Test all token types work
token_types = [:password_reset, :invitation_login, :magic_login, :invitation]
token_types.each do |token_type|
@user.generate_token_for(token_type)
@user.save!
case token_type
when :password_reset
assert @user.password_reset_token.present?
assert @user.password_reset_token_valid?
when :invitation_login
assert @user.invitation_login_token.present?
assert @user.invitation_login_token_valid?
when :magic_login
assert @user.magic_login_token.present?
assert @user.magic_login_token_valid?
when :invitation
assert @user.invitation_token.present?
assert @user.invitation_token_valid?
end
end
end
test "should validate password strength" do
# Test password validation rules
weak_passwords = ["123456", "password", "qwerty", "abc123"]
weak_passwords.each do |password|
user = User.new(email_address: "test@example.com", password: password)
assert_not user.valid?, "Weak password should be invalid"
assert_includes user.errors[:password].to_s, "too short", "Weak password should be too short"
end
# Test valid password
strong_password = "ThisIsA$tr0ngP@ssw0rd!123"
user = User.new(email_address: "test@example.com", password: strong_password)
assert user.valid?, "Strong password should be valid"
end
test "should handle password confirmation validation" do
# Test password confirmation matching
user = User.new(
email_address: "test@example.com",
password: "password123",
password_confirmation: "password123"
)
assert user.valid?, "Password and confirmation should match"
# Test password confirmation mismatch
user.password_confirmation = "different"
assert_not user.valid?, "Password and confirmation should match"
assert_includes user.errors[:password_confirmation].to_s, "doesn't match"
end
test "should handle password reset controller integration" do
# Test that password reset functionality works with controller integration
original_password = @user.password_digest
# Generate reset token through model
@user.generate_token_for(:password_reset)
@user.save!
reset_token = @user.password_reset_token
assert_not_nil reset_token, "Should generate reset token"
# Verify token is usable in controller flow
found_user = User.find_by_password_reset_token(reset_token)
assert_equal @user, found_user, "Should find user by reset token"
end
test "should handle different user statuses" do
# Test password functionality for different user statuses
active_user = users(:alice)
disabled_user = users(:bob)
disabled_user.status = :disabled
disabled_user.save!
# Active user should be able to reset password
assert active_user.generate_token_for(:password_reset)
assert active_user.save
# Disabled user might still be able to reset password (business logic decision)
# This test documents current behavior - adjust if needed
assert_nothing_raised do
disabled_user.generate_token_for(:password_reset)
disabled_user.save
end
end
test "should validate email format during password operations" do
# Test email format validation
invalid_emails = [
"invalid-email",
"@example.com",
"user@",
"",
nil
]
invalid_emails.each do |email|
user = User.new(email_address: email, password: "password123")
assert_not user.valid?, "Invalid email should be rejected"
assert user.errors[:email_address].present?, "Should have email error"
end
# Test valid email formats
valid_emails = [
"user@example.com",
"user+tag@example.com",
"user.name@example.co.uk",
"123user@example-domain.com"
]
valid_emails.each do |email|
user = User.new(email_address: email, password: "password123")
assert user.valid?, "Valid email should be accepted"
end
end
test "should log password changes appropriately" do
# Test that password changes are logged for audit
original_password = @user.password_digest
# Perform password change directly (bypassing token flow for test)
@user.password = "NewPassword123!"
@user.save!
@user.reload
assert_not_equal original_password, @user.password_digest
assert @user.authenticate("NewPassword123!"), "New password should be valid"
# Test that old password is invalidated
old_password_instance = @user.dup
old_password_instance.password_digest = original_password
assert_not old_password_instance.authenticate("NewPassword123!"), "Old password should not authenticate new instance"
assert_not old_password_instance.authenticate("NewPassword123!"), "Password change should invalidate old sessions"
end
end