Back to Repositories

Testing SMTP Client Endpoint Implementation in Postal Server

This test suite validates the SMTP client endpoint functionality in the Postal mail server, focusing on connection handling, SSL modes, and message delivery. The tests ensure proper initialization, IP address handling, and SMTP session management.

Test Coverage Overview

The test suite provides comprehensive coverage of SMTP endpoint functionality including:

  • IP address type detection (IPv4/IPv6)
  • SMTP session initialization and management
  • SSL/TLS mode handling
  • Error handling and connection recovery
  • HELO hostname configuration

Implementation Analysis

The tests utilize RSpec’s describe/context blocks to organize test scenarios logically. Mocking is extensively used to isolate SMTP client behavior, with careful attention to connection lifecycle management and error conditions.

The implementation leverages RSpec’s let statements for test setup and before blocks for mock configuration.

Technical Details

Key technical components include:

  • Net::SMTP client mocking
  • RSpec mocking and stubbing
  • SSL mode configuration testing
  • Connection timeout handling
  • Source IP address management

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive edge case coverage
  • Proper test isolation through mocking
  • Clear context separation
  • Thorough error scenario testing
  • Consistent test organization

postalserver/postal

spec/lib/smtp_client/endpoint_spec.rb

            
# frozen_string_literal: true

require "rails_helper"

module SMTPClient

  RSpec.describe Endpoint do
    let(:ssl_mode) { SSLModes::AUTO }
    let(:server) { Server.new("mx1.example.com", port: 25, ssl_mode: ssl_mode) }
    let(:ip) { "1.2.3.4" }

    before do
      allow(Net::SMTP).to receive(:new).and_wrap_original do |original_method, *args|
        smtp = original_method.call(*args)
        allow(smtp).to receive(:start)
        allow(smtp).to receive(:started?).and_return(true)
        allow(smtp).to receive(:send_message)
        allow(smtp).to receive(:finish)
        smtp
      end
    end

    subject(:endpoint) { described_class.new(server, ip) }

    describe "#description" do
      it "returns a description for the endpoint" do
        expect(endpoint.description).to eq "1.2.3.4:25 (mx1.example.com)"
      end
    end

    describe "#ipv6?" do
      context "when the IP address is an IPv6 address" do
        let(:ip) { "2a00:67a0:a::1" }

        it "returns true" do
          expect(endpoint.ipv6?).to be true
        end
      end

      context "when the IP address is an IPv4 address" do
        it "returns false" do
          expect(endpoint.ipv6?).to be false
        end
      end
    end

    describe "#ipv4?" do
      context "when the IP address is an IPv4 address" do
        it "returns true" do
          expect(endpoint.ipv4?).to be true
        end
      end

      context "when the IP address is an IPv6 address" do
        let(:ip) { "2a00:67a0:a::1" }

        it "returns false" do
          expect(endpoint.ipv4?).to be false
        end
      end
    end

    describe "#start_smtp_session" do
      context "when given no source IP address" do
        it "creates a new Net::SMTP client with appropriate details" do
          client = endpoint.start_smtp_session
          expect(client.address).to eq "1.2.3.4"
        end

        it "sets the appropriate timeouts from the config" do
          client = endpoint.start_smtp_session
          expect(client.open_timeout).to eq Postal::Config.smtp_client.open_timeout
          expect(client.read_timeout).to eq Postal::Config.smtp_client.read_timeout
        end

        it "does not set a source address" do
          client = endpoint.start_smtp_session
          expect(client.source_address).to be_nil
        end

        it "sets the TLS hostname" do
          client = endpoint.start_smtp_session
          expect(client.tls_hostname).to eq "mx1.example.com"
        end

        it "starts the SMTP client the default HELO" do
          endpoint.start_smtp_session
          expect(endpoint.smtp_client).to have_received(:start).with(Postal::Config.postal.smtp_hostname)
        end

        context "when the SSL mode is Auto" do
          it "enables STARTTLS auto " do
            client = endpoint.start_smtp_session
            expect(client.starttls?).to eq :auto
          end
        end

        context "when the SSL mode is STARTLS" do
          let(:ssl_mode) { SSLModes::STARTTLS }

          it "as starttls as always" do
            client = endpoint.start_smtp_session
            expect(client.starttls?).to eq :always
          end
        end

        context "when the SSL mode is TLS" do
          let(:ssl_mode) { SSLModes::TLS }

          it "as starttls as always" do
            client = endpoint.start_smtp_session
            expect(client.tls?).to be true
          end
        end

        context "when the SSL mode is None" do
          let(:ssl_mode) { SSLModes::NONE }

          it "disables STARTTLS and TLS" do
            client = endpoint.start_smtp_session
            expect(client.starttls?).to be false
            expect(client.tls?).to be false
          end
        end

        context "when the SSL mode is Auto but ssl_allow is false" do
          it "disables STARTTLS and TLS" do
            client = endpoint.start_smtp_session(allow_ssl: false)
            expect(client.starttls?).to be false
            expect(client.tls?).to be false
          end
        end
      end

      context "when given a source IP address" do
        let(:ip_address) { create(:ip_address) }

        context "when the endpoint IP is ipv4" do
          it "sets the source address to the IPv4 address" do
            client = endpoint.start_smtp_session(source_ip_address: ip_address)
            expect(client.source_address).to eq ip_address.ipv4
          end
        end

        context "when the endpoint IP is ipv6" do
          let(:ip) { "2a00:67a0:a::1" }

          it "sets the source address to the IPv6 address" do
            client = endpoint.start_smtp_session(source_ip_address: ip_address)
            expect(client.source_address).to eq ip_address.ipv6
          end
        end

        it "starts the SMTP client with the IP addresses hostname" do
          endpoint.start_smtp_session(source_ip_address: ip_address)
          expect(endpoint.smtp_client).to have_received(:start).with(ip_address.hostname)
        end
      end
    end

    describe "#send_message" do
      context "when the smtp client has not been created" do
        it "raises an error" do
          expect { endpoint.send_message("", "", "") }.to raise_error Endpoint::SMTPSessionNotStartedError
        end
      end

      context "when the smtp client exists but is not started" do
        it "raises an error" do
          endpoint.start_smtp_session
          expect(endpoint.smtp_client).to receive(:started?).and_return(false)
          expect { endpoint.send_message("", "", "") }.to raise_error Endpoint::SMTPSessionNotStartedError
        end
      end

      context "when the smtp client is started" do
        before do
          endpoint.start_smtp_session
        end

        it "resets any previous errors" do
          expect(endpoint.smtp_client).to receive(:rset_errors)
          endpoint.send_message("test message", "[email protected]", "[email protected]")
        end

        it "sends the message to the SMTP client" do
          endpoint.send_message("test message", "[email protected]", "[email protected]")
          expect(endpoint.smtp_client).to have_received(:send_message).with("test message", "[email protected]", ["[email protected]"])
        end

        context "when the connection is reset during sending" do
          before do
            endpoint.start_smtp_session
            allow(endpoint.smtp_client).to receive(:send_message) do
              raise Errno::ECONNRESET
            end
          end

          it "closes the SMTP client" do
            expect(endpoint).to receive(:finish_smtp_session).and_call_original
            endpoint.send_message("test message", "", "")
          end

          it "retries sending the message once" do
            expect(endpoint).to receive(:send_message).twice.and_call_original
            endpoint.send_message("test message", "", "")
          end

          context "if the retry also fails" do
            it "raises the error" do
              allow(endpoint).to receive(:send_message).and_raise(Errno::ECONNRESET)
              expect { endpoint.send_message("test message", "", "") }.to raise_error(Errno::ECONNRESET)
            end
          end
        end
      end
    end

    describe "#reset_smtp_session" do
      it "calls rset on the client" do
        endpoint.start_smtp_session
        expect(endpoint.smtp_client).to receive(:rset)
        endpoint.reset_smtp_session
      end

      context "if there is an error" do
        it "finishes the smtp client" do
          endpoint.start_smtp_session
          allow(endpoint.smtp_client).to receive(:rset).and_raise(StandardError)
          expect(endpoint).to receive(:finish_smtp_session)
          endpoint.reset_smtp_session
        end
      end
    end

    describe "#finish_smtp_session" do
      it "calls finish on the client" do
        endpoint.start_smtp_session
        expect(endpoint.smtp_client).to receive(:finish)
        endpoint.finish_smtp_session
      end

      it "sets the smtp client to nil" do
        endpoint.start_smtp_session
        endpoint.finish_smtp_session
        expect(endpoint.smtp_client).to be_nil
      end

      context "if the client finish raises an error" do
        it "does not raise it" do
          endpoint.start_smtp_session
          allow(endpoint.smtp_client).to receive(:finish).and_raise(StandardError)
          expect { endpoint.finish_smtp_session }.not_to raise_error
        end
      end
    end

    describe ".default_helo_hostname" do
      context "when the configuration specifies a helo hostname" do
        before do
          allow(Postal::Config.dns).to receive(:helo_hostname).and_return("helo.example.com")
        end

        it "returns that" do
          expect(described_class.default_helo_hostname).to eq "helo.example.com"
        end
      end

      context "when the configuration does not specify a helo hostname but has an smtp hostname" do
        before do
          allow(Postal::Config.dns).to receive(:helo_hostname).and_return(nil)
          allow(Postal::Config.postal).to receive(:smtp_hostname).and_return("smtp.example.com")
        end

        it "returns the smtp hostname" do
          expect(described_class.default_helo_hostname).to eq "smtp.example.com"
        end
      end

      context "when the configuration has neither a helo hostname or an smtp hostname" do
        before do
          allow(Postal::Config.dns).to receive(:helo_hostname).and_return(nil)
          allow(Postal::Config.postal).to receive(:smtp_hostname).and_return(nil)
        end

        it "returns localhost" do
          expect(described_class.default_helo_hostname).to eq "localhost"
        end
      end
    end
  end

end