Back to Repositories

Testing Raw Email Sending API Authentication in Postal Server

This test suite validates the Legacy Send API functionality in Postal Server, focusing on raw email sending capabilities and authentication mechanisms. It thoroughly examines various authentication scenarios and email processing requirements.

Test Coverage Overview

The test suite provides comprehensive coverage of the raw email sending API endpoint, including authentication, error handling, and successful email processing scenarios.

  • Authentication validation for API requests
  • Server suspension checks
  • Required parameter validation
  • Email message creation and processing
  • Multiple recipient handling

Implementation Analysis

The implementation uses RSpec request specs to simulate HTTP interactions with the API endpoint. It employs factory-based test data generation and structured context blocks to organize different test scenarios.

The tests utilize RSpec’s expect syntax with detailed matchers for response validation and object attribute verification.

Technical Details

  • RSpec request testing framework
  • Factory-based test data generation
  • JSON response parsing and validation
  • Base64 encoding for email data
  • Mail object manipulation for test data
  • Custom header handling

Best Practices Demonstrated

The test suite exemplifies several testing best practices including isolation of test scenarios, comprehensive error case coverage, and thorough validation of response structures.

  • Structured context organization
  • Detailed error scenario coverage
  • Comprehensive attribute validation
  • Clear test case isolation

postalserver/postal

spec/apis/legacy_api/send/raw_spec.rb

            
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "Legacy Send API", type: :request do
  describe "/api/v1/send/raw" do
    context "when no authentication is provided" do
      it "returns an error" do
        post "/api/v1/send/raw"
        expect(response.status).to eq 200
        parsed_body = JSON.parse(response.body)
        expect(parsed_body["status"]).to eq "error"
        expect(parsed_body["data"]["code"]).to eq "AccessDenied"
      end
    end

    context "when the credential does not match anything" do
      it "returns an error" do
        post "/api/v1/send/raw", headers: { "x-server-api-key" => "invalid" }
        expect(response.status).to eq 200
        parsed_body = JSON.parse(response.body)
        expect(parsed_body["status"]).to eq "error"
        expect(parsed_body["data"]["code"]).to eq "InvalidServerAPIKey"
      end
    end

    context "when the credential belongs to a suspended server" do
      it "returns an error" do
        server = create(:server, :suspended)
        credential = create(:credential, server: server)
        post "/api/v1/send/raw", headers: { "x-server-api-key" => credential.key }
        expect(response.status).to eq 200
        parsed_body = JSON.parse(response.body)
        expect(parsed_body["status"]).to eq "error"
        expect(parsed_body["data"]["code"]).to eq "ServerSuspended"
      end
    end

    context "when the credential is valid" do
      let(:server) { create(:server) }
      let(:credential) { create(:credential, server: server) }
      let(:domain) { create(:domain, owner: server) }
      let(:data) do
        mail = Mail.new
        mail.to = "[email protected]"
        mail.from = "test@#{domain.name}"
        mail.subject = "test"
        mail.text_part = Mail::Part.new
        mail.text_part.body = "plain text"
        mail.html_part = Mail::Part.new
        mail.html_part.content_type = "text/html; charset=UTF-8"
        mail.html_part.body = "<p>html</p>"
        mail
      end
      let(:default_params) do
        {
          mail_from: "test@#{domain.name}",
          rcpt_to: ["[email protected]", "[email protected]"],
          data: Base64.encode64(data.to_s),
          bounce: false
        }
      end
      let(:content_type) { "application/json" }
      let(:params) { default_params }

      before do
        post "/api/v1/send/raw",
             headers: { "x-server-api-key" => credential.key,
                        "content-type" => content_type },
             params: content_type == "application/json" ? params.to_json : params
      end

      context "when rcpt_to is not provided" do
        let(:params) { default_params.except(:rcpt_to) }

        it "returns an error" do
          parsed_body = JSON.parse(response.body)
          expect(parsed_body["status"]).to eq "parameter-error"
          expect(parsed_body["data"]["message"]).to match(/`rcpt_to` parameter is required but is missing/i)
        end
      end

      context "when mail_from is not provided" do
        let(:params) { default_params.except(:mail_from) }

        it "returns an error" do
          parsed_body = JSON.parse(response.body)
          expect(parsed_body["status"]).to eq "parameter-error"
          expect(parsed_body["data"]["message"]).to match(/`mail_from` parameter is required but is missing/i)
        end
      end

      context "when data is not provided" do
        let(:params) { default_params.except(:data) }

        it "returns an error" do
          parsed_body = JSON.parse(response.body)
          expect(parsed_body["status"]).to eq "parameter-error"
          expect(parsed_body["data"]["message"]).to match(/`data` parameter is required but is missing/i)
        end
      end

      context "when no recipients are provided" do
        let(:params) { default_params.merge(rcpt_to: []) }

        it "returns success but with no messages" do
          parsed_body = JSON.parse(response.body)
          expect(parsed_body["status"]).to eq "success"
          expect(parsed_body["data"]["messages"]).to eq({})
          expect(parsed_body["data"]["message_id"]).to be nil
        end
      end

      context "when a valid email is provided" do
        it "returns details of the messages created" do
          parsed_body = JSON.parse(response.body)
          expect(parsed_body["data"]["message_id"]).to be_a String
          expect(parsed_body["data"]["messages"]).to be_a Hash
          expect(parsed_body["data"]["messages"]).to match({
            "[email protected]" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ },
            "[email protected]" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }
          })
        end

        it "creates appropriate message objects" do
          parsed_body = JSON.parse(response.body)
          ["[email protected]", "[email protected]"].each do |rcpt_to|
            message_id = parsed_body["data"]["messages"][rcpt_to]["id"]
            message = server.message(message_id)
            expect(message).to have_attributes(
              server: server,
              rcpt_to: rcpt_to,
              mail_from: "test@#{domain.name}",
              subject: "test",
              message_id: kind_of(String),
              timestamp: kind_of(Time),
              domain_id: domain.id,
              credential_id: credential.id,
              bounce: false,
              headers: hash_including("to" => ["[email protected]"]),
              plain_body: "plain text",
              html_body: "<p>html</p>",
              attachments: [],
              received_with_ssl: true,
              scope: "outgoing",
              raw_message: data.to_s
            )
          end
        end

        context "when params are provided as a param" do
          let(:content_type) { nil }
          let(:params) { { params: default_params.to_json } }

          it "returns details of the messages created" do
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["data"]["message_id"]).to be_a String
            expect(parsed_body["data"]["messages"]).to be_a Hash
            expect(parsed_body["data"]["messages"]).to match({
              "[email protected]" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ },
              "[email protected]" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }
            })
          end
        end
      end
    end
  end
end