Back to Repositories

Testing Server Model Configuration and Message Handling in Postal

This comprehensive RSpec test suite validates the Server model functionality in the Postal mail server application. The tests cover essential server configurations, relationships, and message handling capabilities.

Test Coverage Overview

The test suite provides extensive coverage of server model functionality including:

  • Database relationships and associations validation
  • Server configuration validation including name, permalink, and IP pool settings
  • Message handling and throughput monitoring
  • Domain authentication and verification workflows
  • Server suspension and limit management

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with factory-based test data generation. Key implementation aspects include:

  • Extensive use of RSpec matchers for validation testing
  • Factory Bot fixtures for consistent test data
  • Shared context for common server states
  • Isolated unit tests with proper mocking of dependencies

Technical Details

Testing infrastructure includes:

  • RSpec Rails framework integration
  • Factory Bot for test data management
  • Database cleaner for test isolation
  • ActiveRecord test helpers
  • Custom message factory for mail testing

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive model validation testing
  • Edge case coverage for critical functionality
  • Proper separation of concerns in test organization
  • Effective use of RSpec’s subject and let syntax
  • Clear test descriptions and contexts

postalserver/postal

spec/models/server_spec.rb

            
# frozen_string_literal: true

# == Schema Information
#
# Table name: servers
#
#  id                                 :integer          not null, primary key
#  allow_sender                       :boolean          default(FALSE)
#  deleted_at                         :datetime
#  domains_not_to_click_track         :text(65535)
#  log_smtp_data                      :boolean          default(FALSE)
#  message_retention_days             :integer
#  mode                               :string(255)
#  name                               :string(255)
#  outbound_spam_threshold            :decimal(8, 2)
#  permalink                          :string(255)
#  postmaster_address                 :string(255)
#  privacy_mode                       :boolean          default(FALSE)
#  raw_message_retention_days         :integer
#  raw_message_retention_size         :integer
#  send_limit                         :integer
#  send_limit_approaching_at          :datetime
#  send_limit_approaching_notified_at :datetime
#  send_limit_exceeded_at             :datetime
#  send_limit_exceeded_notified_at    :datetime
#  spam_failure_threshold             :decimal(8, 2)
#  spam_threshold                     :decimal(8, 2)
#  suspended_at                       :datetime
#  suspension_reason                  :string(255)
#  token                              :string(255)
#  uuid                               :string(255)
#  created_at                         :datetime
#  updated_at                         :datetime
#  ip_pool_id                         :integer
#  organization_id                    :integer
#
# Indexes
#
#  index_servers_on_organization_id  (organization_id)
#  index_servers_on_permalink        (permalink)
#  index_servers_on_token            (token)
#  index_servers_on_uuid             (uuid)
#
require "rails_helper"

describe Server do
  subject(:server) { build(:server) }

  describe "relationships" do
    it { is_expected.to belong_to(:organization) }
    it { is_expected.to belong_to(:ip_pool).optional }
    it { is_expected.to have_many(:domains) }
    it { is_expected.to have_many(:credentials) }
    it { is_expected.to have_many(:smtp_endpoints) }
    it { is_expected.to have_many(:http_endpoints) }
    it { is_expected.to have_many(:address_endpoints) }
    it { is_expected.to have_many(:routes) }
    it { is_expected.to have_many(:queued_messages) }
    it { is_expected.to have_many(:webhooks) }
    it { is_expected.to have_many(:webhook_requests) }
    it { is_expected.to have_many(:track_domains) }
    it { is_expected.to have_many(:ip_pool_rules) }
  end

  describe "validations" do
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:organization_id).case_insensitive }
    it { is_expected.to validate_inclusion_of(:mode).in_array(Server::MODES) }
    it { is_expected.to validate_uniqueness_of(:permalink).scoped_to(:organization_id).case_insensitive }
    it { is_expected.to validate_exclusion_of(:permalink).in_array(Server::RESERVED_PERMALINKS) }
    it { is_expected.to allow_value("hello").for(:permalink) }
    it { is_expected.to allow_value("hello-world").for(:permalink) }
    it { is_expected.to allow_value("hello1234").for(:permalink) }
    it { is_expected.not_to allow_value("LARGE").for(:permalink) }
    it { is_expected.not_to allow_value(" lots of spaces ").for(:permalink) }
    it { is_expected.not_to allow_value("hello+").for(:permalink) }
    it { is_expected.not_to allow_value("!!!").for(:permalink) }
    it { is_expected.not_to allow_value("[hello]").for(:permalink) }

    describe "ip pool validation" do
      let(:org) { create(:organization) }
      let(:ip_pool) { create(:ip_pool) }
      let(:server) { build(:server, organization: org, ip_pool: ip_pool) }

      context "when the IP pool does not belong to the same organization" do
        it "adds an error" do
          expect(server.save).to be false
          expect(server.errors[:ip_pool_id]).to include(/must belong to the organization/)
        end
      end

      context "whent he IP pool does belong to the the same organization" do
        before do
          org.ip_pools << ip_pool
        end

        it "does not add an error" do
          expect(server.save).to be true
        end
      end
    end
  end

  describe "creation" do
    let(:server) { build(:server) }

    it "generates a uuid" do
      expect { server.save }.to change { server.uuid }.from(nil).to(/[a-f0-9-]{36}/)
    end

    it "generates a token" do
      expect { server.save }.to change { server.token }.from(nil).to(/[a-z0-9]{6}/)
    end

    it "provisions a database" do
      expect(server.message_db.provisioner).to receive(:provision).once
      server.provision_database = true
      server.save
    end
  end

  describe "deletion" do
    it "removes the database" do
      expect(server.message_db.provisioner).to receive(:drop).once
      server.provision_database = true
      server.destroy
    end
  end

  describe "#status" do
    context "when the server is suspended" do
      let(:server) { build(:server, :suspended) }

      it "returns Suspended" do
        expect(server.status).to eq("Suspended")
      end
    end

    context "when the server is not suspended" do
      it "returns the mode" do
        expect(server.status).to eq "Live"
      end
    end
  end

  describe "#full_permalink" do
    it "returns the org and server permalinks concatenated" do
      expect(server.full_permalink).to eq "#{server.organization.permalink}/#{server.permalink}"
    end
  end

  describe "#suspended?" do
    context "when the server is suspended" do
      let(:server) { build(:server, :suspended) }

      it "returns true" do
        expect(server).to be_suspended
      end
    end

    context "when the server is not suspended" do
      it "returns false" do
        expect(server).not_to be_suspended
      end
    end
  end

  describe "#actual_suspension_reason" do
    context "when the server is not suspended" do
      it "returns nil" do
        expect(server.actual_suspension_reason).to be_nil
      end
    end

    context "when the server is not suspended by the organization is" do
      let(:org) { build(:organization, :suspended, suspension_reason: "org test") }
      let(:server) { build(:server, organization: org) }

      it "returns the organization suspension reason" do
        expect(server.actual_suspension_reason).to eq "org test"
      end
    end

    context "when the server is suspended" do
      let(:server) { build(:server, :suspended, suspension_reason: "server test") }

      it "returns the suspension reason" do
        expect(server.actual_suspension_reason).to eq "server test"
      end
    end
  end

  describe "#to_param" do
    it "returns the permalink" do
      expect(server.to_param).to eq server.permalink
    end
  end

  describe "#message_db" do
    it "returns a message DB instance" do
      expect(server.message_db).to be_a Postal::MessageDB::Database
      expect(server.message_db).to have_attributes(server_id: server.id, organization_id: server.organization.id)
    end

    it "caches the value" do
      call1 = server.message_db
      call2 = server.message_db
      expect(call1.object_id).to eq(call2.object_id)
    end
  end

  describe "#message" do
    it "delegates to the message db" do
      expect(server.message_db).to receive(:message).with(1)
      server.message(1)
    end
  end

  describe "#message_rate" do
    it "returns the live stats for the last hour per minute" do
      allow(server.message_db.live_stats).to receive(:total).and_return(600)
      expect(server.message_rate).to eq 10
      expect(server.message_db.live_stats).to have_received(:total).with(60, types: [:incoming, :outgoing])
    end
  end

  describe "#held_messages" do
    it "returns the number of held messages" do
      expect(server.message_db).to receive(:messages).with(count: true, where: { held: true }).and_return(50)
      expect(server.held_messages).to eq 50
    end
  end

  describe "#throughput_stats" do
    before do
      allow(server.message_db.live_stats).to receive(:total).with(60, types: [:incoming]).and_return(50)
      allow(server.message_db.live_stats).to receive(:total).with(60, types: [:outgoing]).and_return(100)
    end

    context "when the server has a sent limit" do
      let(:server) { build(:server, send_limit: 500) }

      it "returns the stats with an outgoing usage percentage" do
        expect(server.throughput_stats).to eq({
          incoming: 50,
          outgoing: 100,
          outgoing_usage: 20.0
        })
      end
    end

    context "when the server does not have a sent limit" do
      it "returns the stats with no outgoing usage percentage" do
        expect(server.throughput_stats).to eq({
          incoming: 50,
          outgoing: 100,
          outgoing_usage: 0
        })
      end
    end
  end

  describe "#bounce_rate" do
    context "when there are no outgoing emails" do
      it "returns zero" do
        expect(server.bounce_rate).to eq 0
      end
    end

    context "when there are outgoing emails with some bounces" do
      it "returns the rate" do
        allow(server.message_db.statistics).to receive(:get).with(:daily, [:outgoing, :bounces], kind_of(Time), 30)
                                                            .and_return({
                                                                          10.minutes.ago => { outgoing: 150, bounces: 50 },
                                                                          5.minutes.ago => { outgoing: 350, bounces: 30 },
                                                                          1.minutes.ago => { outgoing: 500, bounces: 20 }
                                                                        })
        expect(server.bounce_rate).to eq 10.0
      end
    end
  end

  describe "#domain_stats" do
    it "returns stats about the domains associated with the server" do
      create(:domain, owner: server) # verified, bad dns
      create(:domain, :unverified, owner: server) # unverified
      create(:domain, :dns_all_ok, owner: server) # verified good dns

      expect(server.domain_stats).to eq [3, 1, 1]
    end
  end

  describe "#webhook_hash" do
    it "returns a hash to represent the server" do
      expect(server.webhook_hash).to eq({
        uuid: server.uuid,
        name: server.name,
        permalink: server.permalink,
        organization: server.organization.permalink
      })
    end
  end

  describe "#send_volume" do
    it "returns the number of outgoing messages sent in the last hour" do
      allow(server.message_db.live_stats).to receive(:total).with(60, types: [:outgoing]).and_return(50)
      expect(server.send_volume).to eq 50
    end
  end

  describe "#send_limit_approaching?" do
    context "when the server has no send limit" do
      it "returns false" do
        expect(server.send_limit_approaching?).to be false
      end
    end

    context "when the server has a send limit" do
      let(:server) { build(:server, send_limit: 1000) }

      context "when the server's send volume is less 90% of the limit" do
        it "return false" do
          allow(server).to receive(:send_volume).and_return(800)
          expect(server.send_limit_approaching?).to be false
        end
      end

      context "when the server's send volume is more than 90% of the limit" do
        it "returns true" do
          allow(server).to receive(:send_volume).and_return(901)
          expect(server.send_limit_approaching?).to be true
        end
      end
    end
  end

  describe "#send_limit_warning" do
    let(:server) { create(:server, send_limit: 1000) }

    before do
      allow(server).to receive(:send_volume).and_return(500)
    end

    context "when given the :approaching argument" do
      it "sends an email to the org notification addresses" do
        server.organization.users << create(:user)

        server.send_limit_warning(:approaching)
        delivery = ActionMailer::Base.deliveries.last
        expect(delivery).to have_attributes(subject: /mail server is approaching its send limit/i)
      end

      it "sets the notification time" do
        expect { server.send_limit_warning(:approaching) }.to change { server.send_limit_approaching_notified_at }
          .from(nil).to(kind_of(Time))
      end

      it "triggers a webhook" do
        expect(WebhookRequest).to receive(:trigger).with(server, "SendLimitApproaching", server: server.webhook_hash, volume: 500, limit: 1000)
        server.send_limit_warning(:approaching)
      end
    end

    context "when given the :exceeded argument" do
      it "sends an email to the org notification addresses" do
        server.organization.users << create(:user)

        server.send_limit_warning(:exceeded)
        delivery = ActionMailer::Base.deliveries.last
        expect(delivery).to have_attributes(subject: /mail server has exceeded its send limit/i)
      end

      it "sets the notification time" do
        expect { server.send_limit_warning(:exceeded) }.to change { server.send_limit_exceeded_notified_at }
          .from(nil).to(kind_of(Time))
      end

      it "triggers a webhook" do
        expect(WebhookRequest).to receive(:trigger).with(server, "SendLimitExceeded", server: server.webhook_hash, volume: 500, limit: 1000)
        server.send_limit_warning(:exceeded)
      end
    end
  end

  describe "#queue_size" do
    it "returns the number of queued messages that are ready" do
      create(:queued_message, server: server, retry_after: nil)
      create(:queued_message, server: server, retry_after: 1.minute.ago)
      expect(server.queue_size).to eq 2
    end
  end

  describe "#authenticated_domain_for_address" do
    context "when the address given is blank" do
      it "returns nil" do
        expect(server.authenticated_domain_for_address("")).to be nil
        expect(server.authenticated_domain_for_address(nil)).to be nil
      end
    end

    context "when the address given does not have a username & domain component" do
      it "returns nil" do
        expect(server.authenticated_domain_for_address("blah")).to be nil
      end
    end

    context "when there is a verified org-level domain matching the address provided" do
      it "returns that domain" do
        server = create(:server)
        domain = create(:domain, owner: server.organization, name: "mangos.io")
        expect(server.authenticated_domain_for_address("[email protected]")).to eq domain
      end
    end

    context "when there is a verified server-level domain matching the address provided" do
      it "returns that domain" do
        domain = create(:domain, owner: server, name: "oranges.io")
        expect(server.authenticated_domain_for_address("[email protected]")).to eq domain
      end
    end

    context "when there is a verified server-level domain matching the address and a use_for_any" do
      it "returns the matching domain" do
        domain = create(:domain, owner: server, name: "oranges.io")
        create(:domain, owner: server, name: "pears.com", use_for_any: true)
        expect(server.authenticated_domain_for_address("[email protected]")).to eq domain
      end
    end

    context "when there is a verified server-level and org-level domain with the same name" do
      it "returns the server-level domain" do
        domain = create(:domain, owner: server, name: "lemons.com")
        create(:domain, owner: server.organization, name: "lemons.com")
        expect(server.authenticated_domain_for_address("[email protected]")).to eq domain
      end
    end

    context "when there is a verified server-level domain with the 'use_for_any' boolean set with a different name" do
      it "returns that domain" do
        create(:domain, owner: server, name: "pears.com")
        domain = create(:domain, owner: server, name: "apples.io", use_for_any: true)
        expect(server.authenticated_domain_for_address("[email protected]")).to eq domain
      end
    end

    context "when there is no suitable domain" do
      it "returns nil" do
        server = create(:server)
        create(:domain, owner: server, name: "pears.com")
        create(:domain, owner: server.organization, name: "pineapples.com")
        expect(server.authenticated_domain_for_address("[email protected]")).to be nil
      end
    end
  end

  describe "#find_authenticated_domain_from_headers" do
    context "when none of the from addresses have a valid domain" do
      it "returns nil" do
        expect(server.find_authenticated_domain_from_headers("from" => "[email protected]")).to be nil
      end
    end

    context "when the from addresses has a valid domain" do
      it "returns the domain" do
        domain = create(:domain, owner: server)
        expect(server.find_authenticated_domain_from_headers("from" => "hello@#{domain.name}")).to eq domain
      end
    end

    context "when there are multiple from addresses" do
      context "when none of them match a domain" do
        it "returns nil" do
          expect(server.find_authenticated_domain_from_headers("from" => ["[email protected]", "[email protected]"])).to be nil
        end
      end

      context "when some but not all match" do
        it "returns nil" do
          domain = create(:domain, owner: server)
          expect(server.find_authenticated_domain_from_headers("from" => ["hello@#{domain.name}", "[email protected]"])).to be nil
        end
      end

      context "when all match" do
        it "returns the first domain that matched" do
          domain1 = create(:domain, owner: server)
          domain2 = create(:domain, owner: server)
          expect(server.find_authenticated_domain_from_headers("from" => ["hello@#{domain1.name}", "hello@#{domain2.name}"])).to eq domain1
        end
      end
    end

    context "when the server is not allowed to use the sender header" do
      context "when the sender header has a valid address" do
        it "does not return the domain" do
          domain = create(:domain, owner: server)
          result = server.find_authenticated_domain_from_headers(
            "from" => "[email protected]",
            "sender" => "hello@#{domain.name}"
          )
          expect(result).to be nil
        end
      end
    end

    context "when the server is allowed to use the sender header" do
      let(:server) { build(:server, allow_sender: true) }

      context "when none of the from addresses match but sender domains do" do
        it "returns the domain that does match" do
          domain = create(:domain, owner: server)
          result = server.find_authenticated_domain_from_headers(
            "from" => "[email protected]",
            "sender" => "hello@#{domain.name}"
          )
          expect(result).to eq domain
        end
      end
    end
  end

  describe "#suspend" do
    let(:server) { create(:server) }

    it "sets the suspension time" do
      expect { server.suspend("some reason") }.to change { server.reload.suspended_at }.from(nil).to(kind_of(Time))
    end

    it "sets the suspension reason" do
      expect { server.suspend("some reason") }.to change { server.reload.suspension_reason }.from(nil).to("some reason")
    end

    context "when there are no notification addresses" do
      it "does not send an email" do
        server.suspend("some reason")
        expect(ActionMailer::Base.deliveries).to be_empty
      end
    end

    context "when there are notification addresses" do
      before do
        server.organization.users << create(:user)
      end

      it "sends an email" do
        server.suspend("some reason")
        delivery = ActionMailer::Base.deliveries.last
        expect(delivery).to have_attributes(subject: /server has been suspended/i)
      end
    end
  end

  describe "#unsuspend" do
    let(:server) { create(:server, :suspended) }

    it "removes the suspension time" do
      expect { server.unsuspend }.to change { server.reload.suspended_at }.to(nil)
    end

    it "removes the suspension reason" do
      expect { server.unsuspend }.to change { server.reload.suspension_reason }.to(nil)
    end
  end

  describe "#ip_pool_for_message" do
    context "when the message is not outgoing" do
      let(:message) { MessageFactory.incoming(server) }

      it "returns nil" do
        expect(server.ip_pool_for_message(message)).to be nil
      end
    end

    context "when a server rule matches the message" do
      let(:domain) { create(:domain, owner: server) }
      let(:ip_pool) { create(:ip_pool, organizations: [server.organization]) }
      let(:message) do
        MessageFactory.outgoing(server, domain: domain) do |msg|
          msg.rcpt_to = "[email protected]"
        end
      end

      before do
        create(:ip_pool_rule, ip_pool: ip_pool, owner: server, from_text: nil, to_text: "google.com")
      end

      it "returns the pool" do
        expect(server.ip_pool_for_message(message)).to eq ip_pool
      end
    end

    context "when an org rule matches the message" do
      let(:domain) { create(:domain, owner: server) }
      let(:ip_pool) { create(:ip_pool, organizations: [server.organization]) }
      let(:message) do
        MessageFactory.outgoing(server, domain: domain) do |msg|
          msg.rcpt_to = "[email protected]"
        end
      end

      before do
        create(:ip_pool_rule, ip_pool: ip_pool, owner: server.organization, from_text: nil, to_text: "google.com")
      end

      it "returns the pool" do
        expect(server.ip_pool_for_message(message)).to eq ip_pool
      end
    end

    context "when the server has no default pool and no rules match the message" do
      let(:domain) { create(:domain, owner: server) }
      let(:message) { MessageFactory.outgoing(server, domain: domain) }

      it "returns nil" do
        expect(server.ip_pool_for_message(message)).to be nil
      end
    end

    context "when the server has a default pool and no rules match the message" do
      let(:organization) { create(:organization) }
      let(:ip_pool) { create(:ip_pool, organizations: [organization]) }
      let(:server) { create(:server, organization: organization, ip_pool: ip_pool) }
      let(:domain) { create(:domain, owner: server) }
      let(:message) { MessageFactory.outgoing(server, domain: domain) }

      it "returns the server's default pool" do
        expect(server.ip_pool_for_message(message)).to eq ip_pool
      end
    end
  end

  describe ".[]" do
    context "when provided with an integer" do
      it "returns the server with that ID" do
        server = create(:server)
        expect(described_class[server.id]).to eq server
      end

      it "returns nil if no server exists with the ID" do
        expect(described_class[1234]).to be nil
      end
    end

    context "when provided with a string" do
      it "returns the server that matches the given permalinks" do
        server = create(:server)
        expect(described_class["#{server.organization.permalink}/#{server.permalink}"]).to eq server
      end

      it "returns nil if no server exists" do
        expect(described_class["hello/world"]).to be nil
      end
    end
  end
end