diff --git a/test/controllers/tickets_controller_test.rb b/test/controllers/tickets_controller_test.rb index 97a5f36..3024342 100644 --- a/test/controllers/tickets_controller_test.rb +++ b/test/controllers/tickets_controller_test.rb @@ -1,18 +1,63 @@ require "test_helper" class TicketsControllerTest < ActionDispatch::IntegrationTest - test "should get new" do - get tickets_new_url - assert_response :success + include Devise::Test::IntegrationHelpers + setup do + @user = User.create!( + email: "test@example.com", + password: "password123", + password_confirmation: "password123" + ) + + @event = Event.create!( + name: "Test Event", + slug: "test-event", + description: "Valid description for the event that is long enough", + latitude: 48.8566, + longitude: 2.3522, + venue_name: "Test Venue", + venue_address: "123 Test Street", + user: @user + ) + + @order = Order.create!( + user: @user, + event: @event, + total_amount_cents: 1000 + ) + + @ticket = Ticket.create!( + order: @order, + ticket_type: TicketType.create!( + name: "Test Ticket", + description: "Valid description for the ticket type that is long enough", + price_cents: 1000, + quantity: 50, + sale_start_at: Time.current, + sale_end_at: Time.current + 1.day, + requires_id: false, + event: @event + ), + first_name: "Test", + last_name: "User", + qr_code: "test-qr-code" + ) + + sign_in @user end - test "should get create" do - get tickets_create_url - assert_response :success + test "should redirect to checkout" do + get ticket_checkout_path(@event.slug, @event) + assert_response :redirect end - test "should get show" do - get tickets_show_url - assert_response :success + test "should get payment success" do + get payment_success_path(session_id: "test_session") + assert_response :redirect + end + + test "should get payment cancel" do + get payment_cancel_path + assert_response :redirect end end diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index e2fb610..8d562ac 100755 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -1,17 +1,19 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -one: - name: Summer Event - slug: summer-event - description: A great summer event with music and drinks +concert_event: + name: Summer Concert + slug: summer-concert + description: A great summer concert with live music and drinks state: published venue_name: Beach Club venue_address: 123 Ocean Drive latitude: 40.7128 longitude: -74.0060 + start_time: <%= 1.week.from_now %> + end_time: <%= 1.week.from_now + 4.hours %> user: one -two: +winter_gala: name: Winter Gala slug: winter-gala description: An elegant winter gala for the holidays @@ -20,4 +22,6 @@ two: venue_address: 456 Park Avenue latitude: 40.7589 longitude: -73.9851 + start_time: <%= 2.weeks.from_now %> + end_time: <%= 2.weeks.from_now + 6.hours %> user: two diff --git a/test/fixtures/orders.yml b/test/fixtures/orders.yml new file mode 100644 index 0000000..9832752 --- /dev/null +++ b/test/fixtures/orders.yml @@ -0,0 +1,29 @@ +paid_order: + user: one + event: concert_event + status: paid + total_amount_cents: 2500 + payment_attempts: 1 + expires_at: <%= 1.hour.from_now %> + created_at: <%= 1.hour.ago %> + updated_at: <%= 1.hour.ago %> + +draft_order: + user: one + event: concert_event + status: draft + total_amount_cents: 2500 + payment_attempts: 0 + expires_at: <%= 25.minutes.from_now %> + created_at: <%= 5.minutes.ago %> + updated_at: <%= 5.minutes.ago %> + +expired_order: + user: two + event: concert_event + status: expired + total_amount_cents: 2500 + payment_attempts: 1 + expires_at: <%= 1.hour.ago %> + created_at: <%= 2.hours.ago %> + updated_at: <%= 1.hour.ago %> \ No newline at end of file diff --git a/test/fixtures/ticket_types.yml b/test/fixtures/ticket_types.yml index 773d788..6041d8b 100755 --- a/test/fixtures/ticket_types.yml +++ b/test/fixtures/ticket_types.yml @@ -1,21 +1,21 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -one: +standard: name: General Admission description: General admission ticket for the event price_cents: 1000 quantity: 100 sale_start_at: <%= 1.day.ago %> sale_end_at: <%= 1.day.from_now %> - event: one + event: concert_event # minimum_age: 18 -two: +vip: name: VIP Access description: VIP access ticket with special privileges price_cents: 2500 quantity: 50 sale_start_at: <%= 1.day.ago %> sale_end_at: <%= 1.day.from_now %> - event: two + event: concert_event # minimum_age: 18 diff --git a/test/fixtures/tickets.yml b/test/fixtures/tickets.yml index f5ddb84..2393e50 100755 --- a/test/fixtures/tickets.yml +++ b/test/fixtures/tickets.yml @@ -2,14 +2,27 @@ one: qr_code: QR001 - user: one - ticket_type: one + order: paid_order + ticket_type: standard + first_name: John + last_name: Doe price_cents: 1000 status: active two: qr_code: QR002 - user: two - ticket_type: two + order: paid_order + ticket_type: vip + first_name: Jane + last_name: Smith price_cents: 1500 status: active + +draft_ticket: + qr_code: QR003 + order: draft_order + ticket_type: standard + first_name: Bob + last_name: Wilson + price_cents: 1000 + status: draft diff --git a/test/jobs/cleanup_expired_drafts_job_test.rb b/test/jobs/cleanup_expired_drafts_job_test.rb new file mode 100644 index 0000000..d416508 --- /dev/null +++ b/test/jobs/cleanup_expired_drafts_job_test.rb @@ -0,0 +1,166 @@ +require "test_helper" + +class CleanupExpiredDraftsJobTest < ActiveJob::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, + start_time: 1.week.from_now, + end_time: 1.week.from_now + 3.hours, + state: :published + ) + + @ticket_type = TicketType.create!( + name: "General Admission", + description: "General admission tickets with full access to the event", + price_cents: 2500, + quantity: 100, + sale_start_at: Time.current, + sale_end_at: @event.start_time - 1.hour, + requires_id: false, + event: @event + ) + + @order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500 + ) + end + + test "should be queued on default queue" do + assert_equal :default, CleanupExpiredDraftsJob.queue_name + end + + test "should perform job without errors when no tickets exist" do + # Clear all tickets + Ticket.destroy_all + + assert_nothing_raised do + CleanupExpiredDraftsJob.perform_now + end + end + + test "should process expired draft tickets" do + # Create an expired draft ticket + expired_ticket = Ticket.create!( + order: @order, + ticket_type: @ticket_type, + status: "draft", + first_name: "John", + last_name: "Doe" + ) + + # Mock the expired_drafts scope to return our ticket + expired_tickets_relation = Ticket.where(id: expired_ticket.id) + Ticket.expects(:expired_drafts).returns(expired_tickets_relation) + + # Mock the expire_if_overdue! method + expired_ticket.expects(:expire_if_overdue!).once + + CleanupExpiredDraftsJob.perform_now + end + + test "should log information about expired tickets" do + # Create an expired draft ticket + expired_ticket = Ticket.create!( + order: @order, + ticket_type: @ticket_type, + status: "draft", + first_name: "John", + last_name: "Doe" + ) + + # Mock the expired_drafts scope + expired_tickets_relation = Ticket.where(id: expired_ticket.id) + Ticket.expects(:expired_drafts).returns(expired_tickets_relation) + + # Mock the expire_if_overdue! method + expired_ticket.stubs(:expire_if_overdue!) + + # Mock Rails logger + Rails.logger.expects(:info).with("Expiring draft ticket #{expired_ticket.id} for user #{expired_ticket.user.id}") + Rails.logger.expects(:info).with("Expired 1 draft tickets") + + CleanupExpiredDraftsJob.perform_now + end + + test "should handle multiple expired tickets" do + # Create multiple expired draft tickets + 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" + ) + + expired_tickets_relation = Ticket.where(id: [ticket1.id, ticket2.id]) + Ticket.expects(:expired_drafts).returns(expired_tickets_relation) + + ticket1.expects(:expire_if_overdue!).once + ticket2.expects(:expire_if_overdue!).once + + Rails.logger.expects(:info).with("Expiring draft ticket #{ticket1.id} for user #{ticket1.user.id}") + Rails.logger.expects(:info).with("Expiring draft ticket #{ticket2.id} for user #{ticket2.user.id}") + Rails.logger.expects(:info).with("Expired 2 draft tickets") + + CleanupExpiredDraftsJob.perform_now + end + + test "should not log when no tickets are expired" do + # Mock empty expired_drafts scope + empty_relation = Ticket.none + Ticket.expects(:expired_drafts).returns(empty_relation) + + # Should not log the "Expired X tickets" message + Rails.logger.expects(:info).never + + CleanupExpiredDraftsJob.perform_now + end + + test "should handle errors gracefully during ticket processing" do + # Create an expired draft ticket + expired_ticket = Ticket.create!( + order: @order, + ticket_type: @ticket_type, + status: "draft", + first_name: "John", + last_name: "Doe" + ) + + expired_tickets_relation = Ticket.where(id: expired_ticket.id) + Ticket.expects(:expired_drafts).returns(expired_tickets_relation) + + # Mock expire_if_overdue! to raise an error + expired_ticket.expects(:expire_if_overdue!).raises(StandardError.new("Test error")) + + Rails.logger.expects(:info).with("Expiring draft ticket #{expired_ticket.id} for user #{expired_ticket.user.id}") + + # Job should handle the error gracefully and not crash + assert_raises(StandardError) do + CleanupExpiredDraftsJob.perform_now + end + end +end \ No newline at end of file diff --git a/test/jobs/expired_orders_cleanup_job_test.rb b/test/jobs/expired_orders_cleanup_job_test.rb index 1703da2..73bbfde 100644 --- a/test/jobs/expired_orders_cleanup_job_test.rb +++ b/test/jobs/expired_orders_cleanup_job_test.rb @@ -1,7 +1,211 @@ require "test_helper" class ExpiredOrdersCleanupJobTest < ActiveJob::TestCase - # test "the truth" do - # assert true - # end + 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, + start_time: 1.week.from_now, + end_time: 1.week.from_now + 3.hours, + state: :published + ) + end + + test "should be queued on default queue" do + assert_equal :default, ExpiredOrdersCleanupJob.queue_name + end + + test "should perform job without errors when no orders exist" do + # Clear all orders + Order.destroy_all + + assert_nothing_raised do + ExpiredOrdersCleanupJob.perform_now + end + end + + test "should process expired draft orders" do + # Create an expired draft order + expired_order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 1.hour.ago + ) + + # Mock the expired_drafts scope to return our order + expired_orders_relation = Order.where(id: expired_order.id) + Order.expects(:expired_drafts).returns(expired_orders_relation) + + # Mock the expire_if_overdue! method + expired_order.expects(:expire_if_overdue!).once + + # Mock logging + Rails.logger.expects(:info).with("Found 1 expired orders to process") + Rails.logger.expects(:info).with("Expired order ##{expired_order.id} for user ##{expired_order.user_id}") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + ExpiredOrdersCleanupJob.perform_now + end + + test "should handle multiple expired orders" do + # Create multiple expired orders + order1 = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 2.hours.ago + ) + + order2 = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 1500, + expires_at: 1.hour.ago + ) + + expired_orders_relation = Order.where(id: [order1.id, order2.id]) + Order.expects(:expired_drafts).returns(expired_orders_relation) + + order1.expects(:expire_if_overdue!).once + order2.expects(:expire_if_overdue!).once + + Rails.logger.expects(:info).with("Found 2 expired orders to process") + Rails.logger.expects(:info).with("Expired order ##{order1.id} for user ##{order1.user_id}") + Rails.logger.expects(:info).with("Expired order ##{order2.id} for user ##{order2.user_id}") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + ExpiredOrdersCleanupJob.perform_now + end + + test "should handle errors gracefully during order processing" do + # Create an expired order + expired_order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 1.hour.ago + ) + + expired_orders_relation = Order.where(id: expired_order.id) + Order.expects(:expired_drafts).returns(expired_orders_relation) + + # Mock expire_if_overdue! to raise an error + expired_order.expects(:expire_if_overdue!).raises(StandardError.new("Database error")) + + Rails.logger.expects(:info).with("Found 1 expired orders to process") + Rails.logger.expects(:error).with("Failed to expire order ##{expired_order.id}: Database error") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + # Job should handle the error gracefully and continue + assert_nothing_raised do + ExpiredOrdersCleanupJob.perform_now + end + end + + test "should continue processing after individual order failure" do + # Create multiple orders, one will fail + failing_order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 2.hours.ago + ) + + successful_order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 1500, + expires_at: 1.hour.ago + ) + + expired_orders_relation = Order.where(id: [failing_order.id, successful_order.id]) + Order.expects(:expired_drafts).returns(expired_orders_relation) + + # First order fails, second succeeds + failing_order.expects(:expire_if_overdue!).raises(StandardError.new("Test error")) + successful_order.expects(:expire_if_overdue!).once + + Rails.logger.expects(:info).with("Found 2 expired orders to process") + Rails.logger.expects(:error).with("Failed to expire order ##{failing_order.id}: Test error") + Rails.logger.expects(:info).with("Expired order ##{successful_order.id} for user ##{successful_order.user_id}") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + assert_nothing_raised do + ExpiredOrdersCleanupJob.perform_now + end + end + + test "should log count of expired orders found" do + # Create some orders in expired_drafts scope + order1 = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 1.hour.ago + ) + + expired_orders_relation = Order.where(id: order1.id) + Order.expects(:expired_drafts).returns(expired_orders_relation) + order1.stubs(:expire_if_overdue!) + + Rails.logger.expects(:info).with("Found 1 expired orders to process") + Rails.logger.expects(:info).with("Expired order ##{order1.id} for user ##{order1.user_id}") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + ExpiredOrdersCleanupJob.perform_now + end + + test "should handle empty expired orders list" do + # Mock empty expired_drafts scope + empty_relation = Order.none + Order.expects(:expired_drafts).returns(empty_relation) + + Rails.logger.expects(:info).with("Found 0 expired orders to process") + Rails.logger.expects(:info).with("Completed expired orders cleanup job") + + ExpiredOrdersCleanupJob.perform_now + end + + test "should use find_each for memory efficiency" do + # Create an order + order = Order.create!( + user: @user, + event: @event, + status: "draft", + total_amount_cents: 2500, + expires_at: 1.hour.ago + ) + + expired_orders_relation = mock("expired_orders_relation") + expired_orders_relation.expects(:count).returns(1) + expired_orders_relation.expects(:find_each).yields(order) + + Order.expects(:expired_drafts).returns(expired_orders_relation) + + order.expects(:expire_if_overdue!).once + + Rails.logger.stubs(:info) + + ExpiredOrdersCleanupJob.perform_now + end end diff --git a/test/jobs/stripe_invoice_generation_job_test.rb b/test/jobs/stripe_invoice_generation_job_test.rb new file mode 100644 index 0000000..9f5c256 --- /dev/null +++ b/test/jobs/stripe_invoice_generation_job_test.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class StripeInvoiceGenerationJobTest < ActiveJob::TestCase + setup do + @paid_order = orders(:paid_order) + end + + test "should schedule job" do + assert_enqueued_with(job: StripeInvoiceGenerationJob, args: [ @paid_order.id ]) do + StripeInvoiceGenerationJob.perform_later(@paid_order.id) + end + end + + test "should not create invoice for unpaid order" do + draft_order = orders(:draft_order) + + # Should not raise error, just log warning and return + assert_nothing_raised do + StripeInvoiceGenerationJob.perform_now(draft_order.id) + end + end + + test "should handle non-existent order gracefully" do + non_existent_id = 99999 + + # Should not raise error, just log error and return + assert_nothing_raised do + StripeInvoiceGenerationJob.perform_now(non_existent_id) + end + end + + test "should be configured with correct queue" do + job = StripeInvoiceGenerationJob.new + assert_equal :default, job.queue_name.to_sym + end +end diff --git a/test/services/stripe_invoice_service_test.rb b/test/services/stripe_invoice_service_test.rb new file mode 100644 index 0000000..8374ef0 --- /dev/null +++ b/test/services/stripe_invoice_service_test.rb @@ -0,0 +1,316 @@ +require "test_helper" + +class StripeInvoiceServiceTest < ActiveSupport::TestCase + def setup + @user = User.create!( + email: "test@example.com", + password: "password123", + first_name: "John", + last_name: "Doe" + ) + + @event = Event.create!( + name: "Test Concert", + slug: "test-concert", + description: "A test event", + state: "published", + venue_name: "Test Venue", + venue_address: "123 Test St", + latitude: 40.7128, + longitude: -74.0060, + start_time: 1.week.from_now, + end_time: 1.week.from_now + 4.hours, + user: @user + ) + + @ticket_type = @event.ticket_types.create!( + name: "Standard", + description: "Standard admission ticket with general access", + price_cents: 1000, + quantity: 100, + sale_start_at: 1.day.ago, + sale_end_at: 1.day.from_now + ) + + @order = @user.orders.create!( + event: @event, + status: "paid", + total_amount_cents: 1000 + ) + + @ticket = @order.tickets.create!( + ticket_type: @ticket_type, + first_name: "John", + last_name: "Doe", + status: "active", + price_cents: 1000 + ) + + @service = StripeInvoiceService.new(@order) + end + + test "should validate order requirements" do + # Test with nil order + service = StripeInvoiceService.new(nil) + result = service.create_post_payment_invoice + assert_nil result + assert_includes service.errors, "Order is required" + + # Test with unpaid order + draft_order = @user.orders.create!( + event: @event, + status: "draft", + total_amount_cents: 1000 + ) + service = StripeInvoiceService.new(draft_order) + result = service.create_post_payment_invoice + assert_nil result + assert_includes service.errors, "Order must be paid to create invoice" + end + + test "should return error for order without tickets" do + order_without_tickets = @user.orders.create!( + event: @event, + status: "paid", + total_amount_cents: 0 + ) + + service = StripeInvoiceService.new(order_without_tickets) + result = service.create_post_payment_invoice + assert_nil result + assert_includes service.errors, "Order must have tickets to create invoice" + end + + test "get_invoice_pdf_url handles invalid invoice_id gracefully" do + result = StripeInvoiceService.get_invoice_pdf_url("invalid_id") + assert_nil result + + result = StripeInvoiceService.get_invoice_pdf_url(nil) + assert_nil result + + result = StripeInvoiceService.get_invoice_pdf_url("") + assert_nil result + end + + test "customer_name handles various user data combinations" do + # Test with first and last name + @user.update(first_name: "John", last_name: "Doe") + service = StripeInvoiceService.new(@order) + assert_equal "John Doe", service.send(:customer_name) + + # Test with email only + @user.update(first_name: nil, last_name: nil) + service = StripeInvoiceService.new(@order) + result = service.send(:customer_name) + assert result.present? + assert_includes result.downcase, @user.email.split("@").first.downcase + end + + test "build_line_item_description formats correctly" do + tickets = [ @ticket ] + service = StripeInvoiceService.new(@order) + + description = service.send(:build_line_item_description, @ticket_type, tickets) + assert_includes description, @event.name + assert_includes description, @ticket_type.name + assert_includes description, "€" + end + + # === Additional Comprehensive Tests === + + test "should initialize with correct attributes" do + assert_equal @order, @service.order + assert_empty @service.errors + end + + test "should validate order has user" do + order_without_user = Order.new( + event: @event, + status: "paid", + total_amount_cents: 1000 + ) + order_without_user.save(validate: false) # Skip validations to create invalid state + + service = StripeInvoiceService.new(order_without_user) + result = service.create_post_payment_invoice + + assert_nil result + assert_includes service.errors, "Order must have an associated user" + end + + test "should handle Stripe customer creation with existing customer ID" do + @user.update!(stripe_customer_id: "cus_existing123") + + mock_customer = mock("customer") + mock_customer.stubs(:id).returns("cus_existing123") + + Stripe::Customer.expects(:retrieve).with("cus_existing123").returns(mock_customer) + + # Mock the rest of the invoice creation process + mock_invoice = mock("invoice") + mock_invoice.stubs(:id).returns("in_test123") + mock_invoice.stubs(:finalize_invoice).returns(mock_invoice) + mock_invoice.expects(:pay) + Stripe::Invoice.expects(:create).returns(mock_invoice) + Stripe::InvoiceItem.expects(:create).once + + result = @service.create_post_payment_invoice + assert_not_nil result + end + + test "should handle invalid existing Stripe customer" do + @user.update!(stripe_customer_id: "cus_invalid123") + + # First call fails, then create new customer + Stripe::Customer.expects(:retrieve).with("cus_invalid123").raises(Stripe::InvalidRequestError.new("message", "param")) + + mock_customer = mock("customer") + mock_customer.stubs(:id).returns("cus_new123") + Stripe::Customer.expects(:create).returns(mock_customer) + + # Mock the rest of the invoice creation process + mock_invoice = mock("invoice") + mock_invoice.stubs(:id).returns("in_test123") + mock_invoice.stubs(:finalize_invoice).returns(mock_invoice) + mock_invoice.expects(:pay) + Stripe::Invoice.expects(:create).returns(mock_invoice) + Stripe::InvoiceItem.expects(:create).once + + result = @service.create_post_payment_invoice + assert_not_nil result + + @user.reload + assert_equal "cus_new123", @user.stripe_customer_id + end + + test "should handle multiple tickets of same type" do + # Create another ticket of the same type + ticket2 = @order.tickets.create!( + ticket_type: @ticket_type, + first_name: "Jane", + last_name: "Doe", + status: "active", + price_cents: 1000 + ) + + mock_customer = mock("customer") + mock_customer.stubs(:id).returns("cus_test123") + Stripe::Customer.expects(:create).returns(mock_customer) + + expected_line_item = { + customer: "cus_test123", + invoice: "in_test123", + amount: @ticket_type.price_cents * 2, # 2 tickets + currency: "eur", + description: "#{@event.name} - #{@ticket_type.name} - (2x €#{@ticket_type.price_cents / 100.0})", + metadata: { + ticket_type_id: @ticket_type.id, + ticket_type_name: @ticket_type.name, + quantity: 2, + unit_price_cents: @ticket_type.price_cents + } + } + + mock_invoice = mock("invoice") + mock_invoice.stubs(:id).returns("in_test123") + mock_invoice.stubs(:finalize_invoice).returns(mock_invoice) + mock_invoice.expects(:pay) + Stripe::Invoice.expects(:create).returns(mock_invoice) + Stripe::InvoiceItem.expects(:create).with(expected_line_item) + + result = @service.create_post_payment_invoice + assert_not_nil result + end + + test "should create invoice with correct metadata" do + mock_customer = mock("customer") + mock_customer.stubs(:id).returns("cus_test123") + Stripe::Customer.expects(:create).returns(mock_customer) + + expected_invoice_data = { + customer: "cus_test123", + collection_method: "send_invoice", + auto_advance: false, + metadata: { + order_id: @order.id, + user_id: @user.id, + event_name: @event.name, + created_by: "aperonight_system", + payment_method: "checkout_session" + }, + description: "Invoice for #{@event.name} - Order ##{@order.id}", + footer: "Thank you for your purchase! This invoice is for your records as payment was already processed.", + due_date: anything + } + + mock_invoice = mock("invoice") + mock_invoice.stubs(:id).returns("in_test123") + mock_invoice.stubs(:finalize_invoice).returns(mock_invoice) + mock_invoice.expects(:pay) + + Stripe::Invoice.expects(:create).with(expected_invoice_data).returns(mock_invoice) + Stripe::InvoiceItem.expects(:create).once + + result = @service.create_post_payment_invoice + assert_not_nil result + end + + test "should handle Stripe errors gracefully" do + Stripe::Customer.expects(:create).raises(Stripe::StripeError.new("Test Stripe error")) + + result = @service.create_post_payment_invoice + + assert_nil result + assert_includes @service.errors, "Stripe invoice creation failed: Test Stripe error" + end + + test "should handle generic errors gracefully" do + Stripe::Customer.expects(:create).raises(StandardError.new("Generic error")) + + result = @service.create_post_payment_invoice + + assert_nil result + assert_includes @service.errors, "Invoice creation failed: Generic error" + end + + test "should finalize and mark invoice as paid" do + mock_customer = mock("customer") + mock_customer.stubs(:id).returns("cus_test123") + Stripe::Customer.expects(:create).returns(mock_customer) + + mock_invoice = mock("invoice") + mock_invoice.stubs(:id).returns("in_test123") + + mock_finalized_invoice = mock("finalized_invoice") + mock_finalized_invoice.expects(:pay).with({ + paid_out_of_band: true, + payment_method: nil + }) + + Stripe::Invoice.expects(:create).returns(mock_invoice) + Stripe::InvoiceItem.expects(:create).once + mock_invoice.expects(:finalize_invoice).returns(mock_finalized_invoice) + + result = @service.create_post_payment_invoice + assert_equal mock_finalized_invoice, result + end + + # === Class Method Tests === + + test "get_invoice_pdf_url should return PDF URL for valid invoice" do + mock_invoice = mock("invoice") + mock_invoice.expects(:invoice_pdf).returns("https://stripe.com/invoice.pdf") + + Stripe::Invoice.expects(:retrieve).with("in_test123").returns(mock_invoice) + + url = StripeInvoiceService.get_invoice_pdf_url("in_test123") + assert_equal "https://stripe.com/invoice.pdf", url + end + + test "get_invoice_pdf_url should handle Stripe errors" do + Stripe::Invoice.expects(:retrieve).with("in_invalid").raises(Stripe::StripeError.new("Not found")) + + url = StripeInvoiceService.get_invoice_pdf_url("in_invalid") + assert_nil url + end +end diff --git a/test/services/ticket_pdf_generator_test.rb b/test/services/ticket_pdf_generator_test.rb new file mode 100644 index 0000000..e4b7f05 --- /dev/null +++ b/test/services/ticket_pdf_generator_test.rb @@ -0,0 +1,288 @@ +require "test_helper" + +class TicketPdfGeneratorTest < 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, + start_time: 1.week.from_now, + end_time: 1.week.from_now + 3.hours, + state: :published + ) + + @ticket_type = TicketType.create!( + name: "General Admission", + description: "General admission tickets with full access to the event", + price_cents: 2500, + quantity: 100, + sale_start_at: Time.current, + sale_end_at: @event.start_time - 1.hour, + requires_id: false, + event: @event + ) + + @order = Order.create!( + user: @user, + event: @event, + status: "paid", + total_amount_cents: 2500 + ) + + @ticket = Ticket.create!( + order: @order, + ticket_type: @ticket_type, + status: "active", + first_name: "John", + last_name: "Doe", + qr_code: "test-qr-code-123" + ) + end + + # === Initialization Tests === + + test "should initialize with ticket" do + generator = TicketPdfGenerator.new(@ticket) + assert_equal @ticket, generator.ticket + end + + # === PDF Generation Tests === + + test "should generate PDF for valid ticket" do + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + assert_not_nil pdf_string + assert_kind_of String, pdf_string + assert pdf_string.length > 0 + + # Check if it starts with PDF header + assert pdf_string.start_with?("%PDF") + end + + test "should include event name in PDF" do + generator = TicketPdfGenerator.new(@ticket) + + # Mock Prawn::Document to capture text calls + mock_pdf = mock("pdf") + mock_pdf.expects(:fill_color).at_least_once + mock_pdf.expects(:font).at_least_once + mock_pdf.expects(:text).with("ApéroNight", align: :center) + mock_pdf.expects(:text).with(@event.name, align: :center) + mock_pdf.expects(:move_down).at_least_once + mock_pdf.expects(:stroke_color).at_least_once + mock_pdf.expects(:rounded_rectangle).at_least_once + mock_pdf.expects(:fill_and_stroke).at_least_once + mock_pdf.expects(:text).with("Ticket Type:", style: :bold) + mock_pdf.expects(:text).with(@ticket_type.name) + mock_pdf.expects(:text).with("Price:", style: :bold) + mock_pdf.expects(:text).with("€#{@ticket.price_euros}") + mock_pdf.expects(:text).with("Date & Time:", style: :bold) + mock_pdf.expects(:text).with(@event.start_time.strftime("%B %d, %Y at %I:%M %p")) + mock_pdf.expects(:text).with("Venue Information") + mock_pdf.expects(:text).with(@event.venue_name, style: :bold) + mock_pdf.expects(:text).with(@event.venue_address) + mock_pdf.expects(:text).with("Ticket QR Code", align: :center) + mock_pdf.expects(:print_qr_code).once + mock_pdf.expects(:text).with("QR Code: #{@ticket.qr_code[0..7]}...", align: :center) + mock_pdf.expects(:horizontal_line).once + mock_pdf.expects(:text).with("This ticket is valid for one entry only.", align: :center) + mock_pdf.expects(:text).with("Present this ticket at the venue entrance.", align: :center) + mock_pdf.expects(:text).with(regexp_matches(/Generated on/), align: :center) + mock_pdf.expects(:cursor).at_least_once.returns(500) + mock_pdf.expects(:render).returns("fake pdf content") + + Prawn::Document.expects(:new).with(page_size: [350, 600], margin: 20).yields(mock_pdf) + + pdf_string = generator.generate + assert_equal "fake pdf content", pdf_string + end + + test "should include ticket type information in PDF" do + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + # Basic check that PDF was generated - actual content validation + # would require parsing the PDF which is complex + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + test "should include price information in PDF" do + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + test "should include venue information in PDF" do + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + test "should include QR code in PDF" do + generator = TicketPdfGenerator.new(@ticket) + + # Mock RQRCode to verify QR code generation + mock_qrcode = mock("qrcode") + RQRCode::QRCode.expects(:new).with(regexp_matches(/ticket_id.*qr_code/)).returns(mock_qrcode) + + pdf_string = generator.generate + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + # === Error Handling Tests === + + test "should raise error when QR code is blank" do + @ticket.update!(qr_code: "") + generator = TicketPdfGenerator.new(@ticket) + + error = assert_raises(RuntimeError) do + generator.generate + end + + assert_equal "Ticket QR code is missing", error.message + end + + test "should raise error when QR code is nil" do + @ticket.update!(qr_code: nil) + generator = TicketPdfGenerator.new(@ticket) + + error = assert_raises(RuntimeError) do + generator.generate + end + + assert_equal "Ticket QR code is missing", error.message + end + + test "should handle missing event gracefully in QR data" do + # Create ticket without proper associations + orphaned_ticket = Ticket.new( + ticket_type: @ticket_type, + status: "active", + first_name: "John", + last_name: "Doe", + qr_code: "test-qr-code-123" + ) + orphaned_ticket.save(validate: false) + + generator = TicketPdfGenerator.new(orphaned_ticket) + + # Should still generate PDF, but QR data will be limited + pdf_string = generator.generate + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + # === QR Code Data Tests === + + test "should generate correct QR code data" do + generator = TicketPdfGenerator.new(@ticket) + + expected_data = { + ticket_id: @ticket.id, + qr_code: @ticket.qr_code, + event_id: @ticket.event.id, + user_id: @ticket.user.id + }.to_json + + # Mock RQRCode to capture the data being passed + RQRCode::QRCode.expects(:new).with(expected_data).returns(mock("qrcode")) + + generator.generate + end + + test "should compact QR code data removing nils" do + # Test with a ticket that has some nil associations + ticket_with_nils = @ticket.dup + ticket_with_nils.order = nil + ticket_with_nils.save(validate: false) + + generator = TicketPdfGenerator.new(ticket_with_nils) + + # Should generate QR data without the nil user_id + expected_data = { + ticket_id: ticket_with_nils.id, + qr_code: ticket_with_nils.qr_code, + event_id: @ticket.event.id + }.to_json + + RQRCode::QRCode.expects(:new).with(expected_data).returns(mock("qrcode")) + + generator.generate + end + + # === Price Display Tests === + + test "should format price correctly in euros" do + # Test different price formats + @ticket.update!(price_cents: 1050) # €10.50 + + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + assert_not_nil pdf_string + assert_equal 10.5, @ticket.price_euros + end + + test "should handle zero price" do + @ticket.update!(price_cents: 0) + + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + assert_not_nil pdf_string + assert_equal 0.0, @ticket.price_euros + end + + # === Date Formatting Tests === + + test "should format event date correctly" do + specific_time = Time.parse("2024-12-25 19:30:00") + @event.update!(start_time: specific_time) + + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + # Just verify PDF generates - date formatting is handled by strftime + assert_not_nil pdf_string + assert pdf_string.length > 0 + end + + # === Integration Tests === + + test "should generate valid PDF with all required elements" do + generator = TicketPdfGenerator.new(@ticket) + pdf_string = generator.generate + + # Basic PDF structure validation + assert_not_nil pdf_string + assert pdf_string.start_with?("%PDF") + assert pdf_string.end_with?("%%EOF\n") + assert pdf_string.length > 1000, "PDF should be substantial in size" + end + + test "should be callable from ticket model" do + # Test the integration with the Ticket model's to_pdf method + pdf_string = @ticket.to_pdf + + assert_not_nil pdf_string + assert pdf_string.start_with?("%PDF") + end +end \ No newline at end of file