🧪 **Test Infrastructure Enhancements:** - Fixed PDF generator tests by stubbing QR code generation properly - Simplified job tests by replacing complex mocking with functional testing - Added missing `expired_drafts` scope to Ticket model for job functionality - Enhanced test coverage across all components 📋 **Specific Component Fixes:** **PDF Generator Tests (17 tests):** - Added QR code mocking to avoid external dependency issues - Fixed price validation issues for zero/low price scenarios - Simplified complex mocking to focus on functional behavior - All tests now pass with proper assertions **Job Tests (14 tests):** - Replaced complex Rails logger mocking with functional testing - Fixed `expired_drafts` scope missing from Ticket model - Simplified ExpiredOrdersCleanupJob tests to focus on core functionality - Simplified CleanupExpiredDraftsJob tests to avoid brittle mocks - All job tests now pass with proper error handling **Model & Service Tests:** - Enhanced Order model tests (42 tests) with comprehensive coverage - Fixed StripeInvoiceService tests with proper Stripe API mocking - Added comprehensive validation and business logic testing - All model tests passing with edge case coverage **Infrastructure:** - Added rails-controller-testing and mocha gems for better test support - Enhanced test helpers with proper Devise integration - Fixed QR code generation in test environment - Added necessary database migrations and schema updates 🎯 **Test Coverage Summary:** - 202+ tests across the entire application - Models: Order (42 tests), Ticket, Event, User coverage - Controllers: Events (17 tests), Orders (21 tests), comprehensive actions - Services: PDF generation, Stripe integration, business logic - Jobs: Background processing, cleanup operations - All major application functionality covered 🔧 **Technical Improvements:** - Replaced fragile mocking with functional testing approaches - Added proper test data setup and teardown - Enhanced error handling and edge case coverage - Improved test maintainability and reliability 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
534 lines
14 KiB
Ruby
534 lines
14 KiB
Ruby
require "test_helper"
|
|
|
|
class OrderTest < ActiveSupport::TestCase
|
|
def setup
|
|
@user = User.create!(
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
password_confirmation: "password123"
|
|
)
|
|
|
|
@event = Event.create!(
|
|
name: "Test Event",
|
|
slug: "test-event",
|
|
description: "A valid description for the test event that is long enough",
|
|
latitude: 48.8566,
|
|
longitude: 2.3522,
|
|
venue_name: "Test Venue",
|
|
venue_address: "123 Test Street",
|
|
user: @user
|
|
)
|
|
end
|
|
|
|
# === Basic Model Tests ===
|
|
|
|
test "should be a class" do
|
|
assert_kind_of Class, Order
|
|
end
|
|
|
|
# === Constants Tests ===
|
|
|
|
test "should have correct constants defined" do
|
|
assert_equal 30.minutes, Order::DRAFT_EXPIRY_TIME
|
|
assert_equal 3, Order::MAX_PAYMENT_ATTEMPTS
|
|
end
|
|
|
|
# === Association Tests ===
|
|
|
|
test "should belong to user" do
|
|
association = Order.reflect_on_association(:user)
|
|
assert_equal :belongs_to, association.macro
|
|
end
|
|
|
|
test "should belong to event" do
|
|
association = Order.reflect_on_association(:event)
|
|
assert_equal :belongs_to, association.macro
|
|
end
|
|
|
|
test "should have many tickets with dependent destroy" do
|
|
association = Order.reflect_on_association(:tickets)
|
|
assert_equal :has_many, association.macro
|
|
assert_equal :destroy, association.options[:dependent]
|
|
end
|
|
|
|
# === Validation Tests ===
|
|
|
|
test "should not save order without user" do
|
|
order = Order.new(event: @event, total_amount_cents: 1000, status: "draft", payment_attempts: 0)
|
|
assert_not order.save
|
|
assert_includes order.errors[:user_id], "can't be blank"
|
|
end
|
|
|
|
test "should not save order without event" do
|
|
order = Order.new(user: @user, total_amount_cents: 1000, status: "draft", payment_attempts: 0)
|
|
assert_not order.save
|
|
assert_includes order.errors[:event_id], "can't be blank"
|
|
end
|
|
|
|
test "should use default status when not provided" do
|
|
order = Order.new(user: @user, event: @event)
|
|
order.save!
|
|
assert_equal "draft", order.status
|
|
end
|
|
|
|
test "should not save order with invalid status" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
total_amount_cents: 1000,
|
|
status: "invalid_status",
|
|
payment_attempts: 0
|
|
)
|
|
assert_not order.save
|
|
assert_includes order.errors[:status], "is not included in the list"
|
|
end
|
|
|
|
test "should save order with valid statuses" do
|
|
valid_statuses = %w[draft pending_payment paid completed cancelled expired]
|
|
|
|
valid_statuses.each do |status|
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
total_amount_cents: 1000,
|
|
status: status,
|
|
payment_attempts: 0
|
|
)
|
|
assert order.save, "Should save with status: #{status}"
|
|
end
|
|
end
|
|
|
|
test "should use default total_amount_cents when not provided" do
|
|
order = Order.new(user: @user, event: @event)
|
|
order.save!
|
|
assert_equal 0, order.total_amount_cents
|
|
end
|
|
|
|
test "should not save order with negative total_amount_cents" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
total_amount_cents: -100
|
|
)
|
|
assert_not order.save
|
|
assert_includes order.errors[:total_amount_cents], "must be greater than or equal to 0"
|
|
end
|
|
|
|
test "should save order with zero total_amount_cents" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
total_amount_cents: 0
|
|
)
|
|
assert order.save
|
|
end
|
|
|
|
test "should use default payment_attempts when not provided" do
|
|
order = Order.new(user: @user, event: @event)
|
|
order.save!
|
|
assert_equal 0, order.payment_attempts
|
|
end
|
|
|
|
test "should not save order with negative payment_attempts" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
payment_attempts: -1
|
|
)
|
|
assert_not order.save
|
|
assert_includes order.errors[:payment_attempts], "must be greater than or equal to 0"
|
|
end
|
|
|
|
# === Callback Tests ===
|
|
|
|
test "should set expiry time for draft order on create" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event
|
|
)
|
|
|
|
assert_nil order.expires_at
|
|
order.save!
|
|
assert_not_nil order.expires_at
|
|
assert_in_delta Time.current + Order::DRAFT_EXPIRY_TIME, order.expires_at, 5.seconds
|
|
end
|
|
|
|
test "should not set expiry time for non-draft order on create" do
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
status: "paid"
|
|
)
|
|
|
|
order.save!
|
|
assert_nil order.expires_at
|
|
end
|
|
|
|
test "should not override existing expires_at on create" do
|
|
custom_expiry = 1.hour.from_now
|
|
order = Order.new(
|
|
user: @user,
|
|
event: @event,
|
|
expires_at: custom_expiry
|
|
)
|
|
|
|
order.save!
|
|
assert_equal custom_expiry.to_i, order.expires_at.to_i
|
|
end
|
|
|
|
# === Scope Tests ===
|
|
|
|
test "draft scope should return only draft orders" do
|
|
draft_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
paid_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0
|
|
)
|
|
|
|
draft_orders = Order.draft
|
|
assert_includes draft_orders, draft_order
|
|
assert_not_includes draft_orders, paid_order
|
|
end
|
|
|
|
test "active scope should return paid and completed orders" do
|
|
draft_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
paid_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0
|
|
)
|
|
completed_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "completed", payment_attempts: 0
|
|
)
|
|
|
|
active_orders = Order.active
|
|
assert_not_includes active_orders, draft_order
|
|
assert_includes active_orders, paid_order
|
|
assert_includes active_orders, completed_order
|
|
end
|
|
|
|
test "expired_drafts scope should return expired draft orders" do
|
|
# Create an expired draft order
|
|
expired_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
# Create a non-expired draft order
|
|
active_draft = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
expired_drafts = Order.expired_drafts
|
|
assert_includes expired_drafts, expired_order
|
|
assert_not_includes expired_drafts, active_draft
|
|
end
|
|
|
|
test "can_retry_payment scope should return retryable orders" do
|
|
# Create a retryable order
|
|
retryable_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 1
|
|
)
|
|
|
|
# Create a non-retryable order (too many attempts)
|
|
max_attempts_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: Order::MAX_PAYMENT_ATTEMPTS
|
|
)
|
|
|
|
# Create an expired order
|
|
expired_order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 1,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
retryable_orders = Order.can_retry_payment
|
|
assert_includes retryable_orders, retryable_order
|
|
assert_not_includes retryable_orders, max_attempts_order
|
|
assert_not_includes retryable_orders, expired_order
|
|
end
|
|
|
|
# === Instance Method Tests ===
|
|
|
|
test "total_amount_euros should convert cents to euros" do
|
|
order = Order.new(total_amount_cents: 1500)
|
|
assert_equal 15.0, order.total_amount_euros
|
|
|
|
order = Order.new(total_amount_cents: 1050)
|
|
assert_equal 10.5, order.total_amount_euros
|
|
end
|
|
|
|
test "can_retry_payment? should return true for retryable orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 1
|
|
)
|
|
|
|
assert order.can_retry_payment?
|
|
end
|
|
|
|
test "can_retry_payment? should return false for non-draft orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 1
|
|
)
|
|
|
|
assert_not order.can_retry_payment?
|
|
end
|
|
|
|
test "can_retry_payment? should return false for max attempts reached" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: Order::MAX_PAYMENT_ATTEMPTS
|
|
)
|
|
|
|
assert_not order.can_retry_payment?
|
|
end
|
|
|
|
test "can_retry_payment? should return false for expired orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 1,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
assert_not order.can_retry_payment?
|
|
end
|
|
|
|
test "expired? should return true for expired orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
assert order.expired?
|
|
end
|
|
|
|
test "expired? should return false for non-expired orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
assert_not order.expired?
|
|
end
|
|
|
|
test "expired? should return false when expires_at is nil" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0
|
|
)
|
|
|
|
assert_not order.expired?
|
|
end
|
|
|
|
test "expire_if_overdue! should mark expired draft as expired" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
order.expire_if_overdue!
|
|
order.reload
|
|
assert_equal "expired", order.status
|
|
end
|
|
|
|
test "expire_if_overdue! should not affect non-draft orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0,
|
|
expires_at: 1.hour.ago
|
|
)
|
|
|
|
order.expire_if_overdue!
|
|
order.reload
|
|
assert_equal "paid", order.status
|
|
end
|
|
|
|
test "expire_if_overdue! should not affect non-expired orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
order.expire_if_overdue!
|
|
order.reload
|
|
assert_equal "draft", order.status
|
|
end
|
|
|
|
test "increment_payment_attempt! should increment counter and set timestamp" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
assert_nil order.last_payment_attempt_at
|
|
|
|
order.increment_payment_attempt!
|
|
order.reload
|
|
|
|
assert_equal 1, order.payment_attempts
|
|
assert_not_nil order.last_payment_attempt_at
|
|
assert_in_delta Time.current, order.last_payment_attempt_at, 5.seconds
|
|
end
|
|
|
|
test "expiring_soon? should return true for orders expiring within 5 minutes" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0,
|
|
expires_at: 3.minutes.from_now
|
|
)
|
|
|
|
assert order.expiring_soon?
|
|
end
|
|
|
|
test "expiring_soon? should return false for orders expiring later" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0,
|
|
expires_at: 10.minutes.from_now
|
|
)
|
|
|
|
assert_not order.expiring_soon?
|
|
end
|
|
|
|
test "expiring_soon? should return false for non-draft orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0,
|
|
expires_at: 3.minutes.from_now
|
|
)
|
|
|
|
assert_not order.expiring_soon?
|
|
end
|
|
|
|
test "expiring_soon? should return false when expires_at is nil" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
order.update_column(:expires_at, nil) # Bypass validation to test edge case
|
|
|
|
assert_not order.expiring_soon?
|
|
end
|
|
|
|
test "mark_as_paid! should update status and activate tickets" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
# Create some tickets for the order
|
|
ticket_type = TicketType.create!(
|
|
name: "Test Ticket Type",
|
|
description: "A valid description for the ticket type that is long enough",
|
|
price_cents: 1000,
|
|
quantity: 10,
|
|
sale_start_at: Time.current,
|
|
sale_end_at: Time.current + 1.day,
|
|
requires_id: false,
|
|
event: @event
|
|
)
|
|
|
|
ticket1 = Ticket.create!(
|
|
order: order,
|
|
ticket_type: ticket_type,
|
|
status: "draft",
|
|
first_name: "John",
|
|
last_name: "Doe"
|
|
)
|
|
|
|
ticket2 = Ticket.create!(
|
|
order: order,
|
|
ticket_type: ticket_type,
|
|
status: "draft",
|
|
first_name: "Jane",
|
|
last_name: "Doe"
|
|
)
|
|
|
|
order.mark_as_paid!
|
|
|
|
order.reload
|
|
ticket1.reload
|
|
ticket2.reload
|
|
|
|
assert_equal "paid", order.status
|
|
assert_equal "active", ticket1.status
|
|
assert_equal "active", ticket2.status
|
|
end
|
|
|
|
test "calculate_total! should sum ticket prices" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 0,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
# Create ticket type and tickets
|
|
ticket_type = TicketType.create!(
|
|
name: "Test Ticket Type",
|
|
description: "A valid description for the ticket type that is long enough",
|
|
price_cents: 1500,
|
|
quantity: 10,
|
|
sale_start_at: Time.current,
|
|
sale_end_at: Time.current + 1.day,
|
|
requires_id: false,
|
|
event: @event
|
|
)
|
|
|
|
Ticket.create!(
|
|
order: order,
|
|
ticket_type: ticket_type,
|
|
status: "draft",
|
|
first_name: "John",
|
|
last_name: "Doe"
|
|
)
|
|
|
|
Ticket.create!(
|
|
order: order,
|
|
ticket_type: ticket_type,
|
|
status: "draft",
|
|
first_name: "Jane",
|
|
last_name: "Doe"
|
|
)
|
|
|
|
order.calculate_total!
|
|
order.reload
|
|
|
|
assert_equal 3000, order.total_amount_cents # 2 tickets * 1500 cents
|
|
end
|
|
|
|
# === Stripe Integration Tests (Mock) ===
|
|
|
|
test "create_stripe_invoice! should return nil for non-paid orders" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "draft", payment_attempts: 0
|
|
)
|
|
|
|
result = order.create_stripe_invoice!
|
|
assert_nil result
|
|
end
|
|
|
|
test "stripe_invoice_pdf_url should return nil when no invoice ID present" do
|
|
order = Order.create!(
|
|
user: @user, event: @event, total_amount_cents: 1000,
|
|
status: "paid", payment_attempts: 0
|
|
)
|
|
|
|
result = order.stripe_invoice_pdf_url
|
|
assert_nil result
|
|
end
|
|
end
|