Back to Repositories

Validating Domain Model Implementation in Postal Server

This comprehensive test suite validates the Domain model functionality in the Postal email server application, covering domain verification, DKIM key management, and DNS record handling. The tests ensure proper domain validation, relationship management, and security features implementation.

Test Coverage Overview

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

  • Domain verification methods (Email and DNS)
  • DKIM key generation and management
  • Domain relationship validations
  • SPF and DKIM record generation
  • Domain name validation and uniqueness checks

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development framework with factory-based test data generation. The suite implements context-specific testing with detailed expectations for domain verification states, DKIM key management, and DNS record validation.

Key patterns include shared examples for verification methods and extensive use of RSpec matchers for validation testing.

Technical Details

Testing tools and configuration:

  • RSpec for test framework
  • FactoryBot for test data generation
  • OpenSSL for DKIM key validation
  • Custom DNS resolver implementation
  • Schema-based model validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive model validation testing
  • Isolated unit tests with proper mocking
  • Clear context separation
  • Edge case coverage
  • Consistent test organization

postalserver/postal

spec/models/domain_spec.rb

            
# frozen_string_literal: true

# == Schema Information
#
# Table name: domains
#
#  id                     :integer          not null, primary key
#  dkim_error             :string(255)
#  dkim_identifier_string :string(255)
#  dkim_private_key       :text(65535)
#  dkim_status            :string(255)
#  dns_checked_at         :datetime
#  incoming               :boolean          default(TRUE)
#  mx_error               :string(255)
#  mx_status              :string(255)
#  name                   :string(255)
#  outgoing               :boolean          default(TRUE)
#  owner_type             :string(255)
#  return_path_error      :string(255)
#  return_path_status     :string(255)
#  spf_error              :string(255)
#  spf_status             :string(255)
#  use_for_any            :boolean
#  uuid                   :string(255)
#  verification_method    :string(255)
#  verification_token     :string(255)
#  verified_at            :datetime
#  created_at             :datetime
#  updated_at             :datetime
#  owner_id               :integer
#  server_id              :integer
#
# Indexes
#
#  index_domains_on_server_id  (server_id)
#  index_domains_on_uuid       (uuid)
#
require "rails_helper"

describe Domain do
  subject(:domain) { build(:domain) }

  describe "relationships" do
    it { is_expected.to belong_to(:server).optional }
    it { is_expected.to belong_to(:owner).optional }
    it { is_expected.to have_many(:routes) }
    it { is_expected.to have_many(:track_domains) }
  end

  describe "validations" do
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to([:owner_type, :owner_id]).case_insensitive.with_message("is already added") }
    it { is_expected.to allow_value("example.com").for(:name) }
    it { is_expected.to allow_value("example.co.uk").for(:name) }
    it { is_expected.to_not allow_value("EXAMPLE.COM").for(:name) }
    it { is_expected.to_not allow_value("example.com ").for(:name) }
    it { is_expected.to_not allow_value("example com").for(:name) }
    it { is_expected.to validate_inclusion_of(:verification_method).in_array(Domain::VERIFICATION_METHODS) }
  end

  describe "creation" do
    it "creates a new dkim identifier string" do
      expect { domain.save }.to change { domain.dkim_identifier_string }.from(nil).to(match(/\A[a-zA-Z0-9]{6}\z/))
    end

    it "generates a new dkim key" do
      expect { domain.save }.to change { domain.dkim_private_key }.from(nil).to(match(/\A-+BEGIN RSA PRIVATE KEY-+/))
    end

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

  describe ".verified" do
    it "returns verified domains only" do
      verified_domain = create(:domain)
      create(:domain, :unverified)
      expect(described_class.verified).to eq [verified_domain]
    end
  end

  context "when verification method changes" do
    context "to DNS" do
      let(:domain) { create(:domain, :unverified, verification_method: "Email") }

      it "generates a DNS suitable verification token" do
        domain.verification_method = "DNS"
        expect { domain.save }.to change { domain.verification_token }.from(match(/\A\d{6}\z/)).to(match(/\A[A-Za-z0-9+]{32}\z/))
      end
    end

    context "to Email" do
      let(:domain) { create(:domain, :unverified, verification_method: "DNS") }

      it "generates an email suitable verification token" do
        domain.verification_method = "Email"
        expect { domain.save }.to change { domain.verification_token }.from(match(/\A[A-Za-z0-9+]{32}\z/)).to(match(/\A\d{6}\z/))
      end
    end
  end

  describe "#verified?" do
    context "when the domain is verified" do
      it "returns true" do
        expect(domain.verified?).to be true
      end
    end

    context "when the domain is not verified" do
      let(:domain) { build(:domain, :unverified) }

      it "returns false" do
        expect(domain.verified?).to be false
      end
    end
  end

  describe "#mark_as_verified" do
    context "when already verified" do
      it "returns false" do
        expect(domain.mark_as_verified).to be false
      end
    end

    context "when unverified" do
      let(:domain) { create(:domain, :unverified) }

      it "sets the verification time" do
        expect { domain.mark_as_verified }.to change { domain.verified_at }.from(nil).to(kind_of(Time))
      end
    end
  end

  describe "#parent_domains" do
    context "at level 1" do
      let(:domain) { build(:domain, name: "example.com") }

      it "returns the current domain only" do
        expect(domain.parent_domains).to eq ["example.com"]
      end
    end

    context "at level 2" do
      let(:domain) { build(:domain, name: "test.example.com") }

      it "returns the current domain plus its parent" do
        expect(domain.parent_domains).to eq ["test.example.com", "example.com"]
      end
    end

    context "at level 3 (and higher)" do
      let(:domain) { build(:domain, name: "sub.test.example.com") }

      it "returns the current domain plus its parents" do
        expect(domain.parent_domains).to eq ["sub.test.example.com", "test.example.com", "example.com"]
      end
    end
  end

  describe "#generate_dkim_key" do
    it "generates a new dkim key" do
      expect { domain.generate_dkim_key }.to change { domain.dkim_private_key }.from(nil).to(match(/\A-+BEGIN RSA PRIVATE KEY-+/))
    end
  end

  describe "#dkim_key" do
    context "when the domain has a DKIM key" do
      let(:domain) { create(:domain) }

      it "returns the dkim key as a OpenSSL::PKey::RSA" do
        expect(domain.dkim_key).to be_a OpenSSL::PKey::RSA
        expect(domain.dkim_key.to_s).to eq domain.dkim_private_key
      end
    end

    context "when the domain has no DKIM key" do
      let(:domain) { build(:domain) }

      it "returns nil" do
        expect(domain.dkim_key).to be_nil
      end
    end
  end

  describe "#to_param" do
    context "when the domain has not been saved" do
      it "returns nil" do
        expect(domain.to_param).to be_nil
      end
    end
    context "when the domain has been saved" do
      before do
        domain.save
      end

      it "returns the UUID" do
        expect(domain.to_param).to eq domain.uuid
      end
    end
  end

  describe "#verification_email_addresses" do
    let(:domain) { build(:domain, name: "example.com") }

    it "returns the verification email addresses" do
      expect(domain.verification_email_addresses).to eq [
        "[email protected]",
        "[email protected]",
        "[email protected]",
        "[email protected]",
        "[email protected]",
      ]
    end
  end

  describe "#spf_record" do
    it "returns the SPF record" do
      expect(domain.spf_record).to eq "v=spf1 a mx include:#{Postal::Config.dns.spf_include} ~all"
    end
  end

  describe "#dkim_record" do
    context "when the domain has no DKIM key" do
      it "returns nil" do
        expect(domain.dkim_record).to be_nil
      end
    end

    context "when the domain has a DKIM key" do
      before do
        domain.save
      end

      it "returns the DKIM record" do
        expect(domain.dkim_record).to match(/\Av=DKIM1; t=s; h=sha256; p=.*;\z/)
      end
    end
  end

  describe "#dkim_identifier" do
    context "when the domain has no dkim identifier string" do
      it "returns nil" do
        expect(domain.dkim_identifier).to be_nil
      end
    end

    context "when the domain has a dkim identifier string" do
      before do
        domain.save
      end

      it "returns the DKIM identifier" do
        expect(domain.dkim_identifier).to eq "#{Postal::Config.dns.dkim_identifier}-#{domain.dkim_identifier_string}"
      end
    end
  end

  describe "#dkim_record_name" do
    context "when the domain has no dkim identifier string" do
      it "returns nil" do
        expect(domain.dkim_record_name).to be_nil
      end
    end

    context "when the domain has a dkim identifier string" do
      before do
        domain.save
      end

      it "returns the DKIM identifier" do
        expect(domain.dkim_record_name).to eq "#{Postal::Config.dns.dkim_identifier}-#{domain.dkim_identifier_string}._domainkey"
      end
    end
  end

  describe "#return_path_domain" do
    it "returns the return path domain" do
      expect(domain.return_path_domain).to eq "#{Postal::Config.dns.custom_return_path_prefix}.#{domain.name}"
    end
  end

  describe "#dns_verification_string" do
    let(:domain) { create(:domain, verification_method: "DNS") }

    it "returns the DNS verification string" do
      expect(domain.dns_verification_string).to eq "#{Postal::Config.dns.domain_verify_prefix} #{domain.verification_token}"
    end
  end

  describe "#resolver" do
    context "when the local nameservers should be used" do
      before do
        allow(Postal::Config.postal).to receive(:use_local_ns_for_domain_verification?).and_return(true)
      end

      it "uses the local DNS" do
        expect(domain.resolver).to eq DNSResolver.local
      end
    end

    context "when local nameservers should not be used" do
      it "uses the a resolver for this domain" do
        allow(DNSResolver).to receive(:for_domain).with(domain.name).and_return(DNSResolver.new(["1.2.3.4"]))
        expect(domain.resolver).to be_a DNSResolver
        expect(domain.resolver.nameservers).to eq ["1.2.3.4"]
      end
    end
  end

  describe "#verify_with_dns" do
    context "when the verification method is not DNS" do
      let(:domain) { build(:domain, verification_method: "Email") }

      it "returns false" do
        expect(domain.verify_with_dns).to be false
      end
    end

    context "when a TXT record is found that matches" do
      let(:domain) { create(:domain, :unverified) }

      before do
        allow(domain.resolver).to receive(:txt).with(domain.name).and_return([domain.dns_verification_string])
      end

      it "returns true" do
        expect(domain.verify_with_dns).to be true
      end

      it "sets the verification time" do
        expect { domain.verify_with_dns }.to change { domain.verified_at }.from(nil).to(kind_of(Time))
      end
    end

    context "when no TXT record is found" do
      let(:domain) { create(:domain, :unverified) }

      before do
        allow(domain.resolver).to receive(:txt).with(domain.name).and_return(["something", "something else"])
      end

      it "returns false" do
        expect(domain.verify_with_dns).to be false
      end

      it "does not set the verification time" do
        expect { domain.verify_with_dns }.to_not change { domain.verified_at } # rubocop:disable Lint/AmbiguousBlockAssociation
      end
    end
  end
end