Back to Repositories

Testing Message Queue Processing Implementation in Postal

This test suite examines the SingleMessageProcessor component in the Postal server, focusing on message queue processing logic. The tests validate various message handling scenarios including suspended servers, maximum attempts, and message type routing.

Test Coverage Overview

The test suite provides comprehensive coverage of the SingleMessageProcessor functionality.

Key areas tested include:
  • Server suspension handling
  • Maximum delivery attempt management
  • Raw message data validation
  • Message type routing (incoming vs outgoing)
Edge cases covered include suspended server states, exceeded delivery attempts, and missing raw message data scenarios.

Implementation Analysis

The testing approach utilizes RSpec’s context-based structure to organize different message processing scenarios. The implementation leverages factory patterns for test data generation and mock objects for isolated testing.

Technical patterns include:
  • Factory-based test data generation
  • Behavior verification using RSpec matchers
  • State management through mock objects
  • Isolated component testing

Technical Details

Testing tools and configuration:
  • RSpec testing framework
  • Factory patterns for object creation
  • TestLogger for logging verification
  • ActiveRecord for database interactions
  • Mock objects for external dependencies

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through well-organized test contexts and comprehensive scenario coverage.

Notable practices include:
  • Isolated test scenarios
  • Descriptive context naming
  • Thorough state verification
  • Clear setup and teardown procedures
  • Effective use of RSpec’s let statements for test data management

postalserver/postal

spec/lib/message_dequeuer/single_message_processor_spec.rb

            
# frozen_string_literal: true

require "rails_helper"

module MessageDequeuer

  RSpec.describe SingleMessageProcessor do
    let(:server) { create(:server) }
    let(:state) { State.new }
    let(:logger) { TestLogger.new }
    let(:route) { create(:route, server: server) }
    let(:message) { MessageFactory.incoming(server, route: route) }
    let(:queued_message) { create(:queued_message, :locked, message: message) }

    subject(:processor) { described_class.new(queued_message, logger: logger, state: state) }

    context "when the server is suspended" do
      before do
        allow(queued_message.server).to receive(:suspended?).and_return(true)
      end

      it "logs" do
        processor.process
        expect(logger).to have_logged(/server is suspended/)
      end

      it "sets the message status to Held" do
        processor.process
        expect(message.reload.status).to eq "Held"
      end

      it "creates a Held delivery" do
        processor.process
        delivery = message.deliveries.last
        expect(delivery).to have_attributes(status: "Held", details: /server has been suspended/i)
      end

      it "removes the queued message" do
        processor.process
        expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    context "when the number of attempts is more than the maximum" do
      let(:queued_message) { create(:queued_message, :locked, message: message, attempts: Postal::Config.postal.default_maximum_delivery_attempts + 1) }

      it "logs" do
        processor.process
        expect(logger).to have_logged(/message has reached maximum number of attempts/)
      end

      it "sends a bounce to the sender" do
        expect(BounceMessage).to receive(:new).with(server, queued_message.message)
        processor.process
      end

      it "sets the message status to HardFail" do
        processor.process
        expect(message.reload.status).to eq "HardFail"
      end

      it "creates a HardFail delivery" do
        processor.process
        delivery = message.deliveries.last
        expect(delivery).to have_attributes(status: "HardFail", details: /maximum number of delivery attempts.*bounce sent to sender/i)
      end

      it "removes the queued message" do
        processor.process
        expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    context "when the message raw data has been removed" do
      before do
        message.raw_table = nil
        message.save
      end

      it "logs" do
        processor.process
        expect(logger).to have_logged(/raw message has been removed/)
      end

      it "sets the message status to HardFail" do
        processor.process
        expect(message.reload.status).to eq "HardFail"
      end

      it "creates a HardFail delivery" do
        processor.process
        delivery = message.deliveries.last
        expect(delivery).to have_attributes(status: "HardFail", details: /Raw message has been removed/i)
      end

      it "removes the queued message" do
        processor.process
        expect { queued_message.reload }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    context "when the message is incoming" do
      it "calls the incoming message processor" do
        expect(IncomingMessageProcessor).to receive(:new).with(queued_message,
                                                               logger: logger,
                                                               state: processor.state)
        processor.process
      end

      it "does not call the outgoing message processor" do
        expect(OutgoingMessageProcessor).to_not receive(:process)
        processor.process
      end
    end

    context "when the message is outgoing" do
      let(:message) { MessageFactory.outgoing(server) }

      it "calls the outgoing message processor" do
        expect(OutgoingMessageProcessor).to receive(:process).with(queued_message,
                                                                   logger: logger,
                                                                   state: processor.state)

        processor.process
      end

      it "does not call the incoming message processor" do
        expect(IncomingMessageProcessor).to_not receive(:process)
        processor.process
      end
    end
  end

end