Back to Repositories

Testing Legacy Messages API Implementation in Postal Server

This test suite validates the Legacy Messages API endpoints in Postal Server, focusing on message retrieval and expansion functionality. It covers authentication, error handling, and various message data expansion scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the /api/v1/messages/message endpoint, examining authentication flows, error conditions, and message retrieval scenarios.

  • Authentication validation including missing, invalid, and suspended credentials
  • Message ID validation and error handling
  • Detailed message data expansion options
  • Activity tracking for message loads and clicks

Implementation Analysis

The implementation uses RSpec request specs to test the API endpoints systematically. It employs context-based organization with nested describe blocks to group related test scenarios.

  • Factory-based test data generation
  • JSON response parsing and validation
  • Dynamic message expansion handling
  • Comprehensive status code and response format verification

Technical Details

  • RSpec for test framework
  • Factory patterns for test data generation
  • JSON parsing for response validation
  • Base64 encoding for raw message handling
  • Database integration for activity tracking

Best Practices Demonstrated

The test suite exemplifies several testing best practices for API validation.

  • Hierarchical test organization using describe/context blocks
  • Comprehensive error case coverage
  • Isolated test scenarios with proper setup
  • Detailed response validation
  • DRY implementation using shared contexts and let blocks

postalserver/postal

spec/apis/legacy_api/messages/message_spec.rb

            
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "Legacy Messages API", type: :request do
  describe "/api/v1/messages/message" do
    context "when no authentication is provided" do
      it "returns an error" do
        post "/api/v1/messages/message"
        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/messages/message", 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/messages/message", 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) }

      context "when no message ID is provided" do
        it "returns an error" do
          post "/api/v1/messages/message", 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 "parameter-error"
          expect(parsed_body["data"]["message"]).to match(/`id` parameter is required but is missing/)
        end
      end

      context "when the message ID does not exist" do
        it "returns an error" do
          post "/api/v1/messages/message",
               headers: { "x-server-api-key" => credential.key,
                          "content-type" => "application/json" },
               params: { id: 123 }.to_json
          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 "MessageNotFound"
        end
      end

      context "when the message ID exists" do
        let(:server) { create(:server) }
        let(:credential) { create(:credential, server: server) }
        let(:message) { MessageFactory.outgoing(server) }
        let(:expansions) { [] }

        before do
          post "/api/v1/messages/message",
               headers: { "x-server-api-key" => credential.key,
                          "content-type" => "application/json" },
               params: { id: message.id, _expansions: expansions }.to_json
        end

        context "when no expansions are requested" do
          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token
            })
          end
        end

        context "when all expansions are requested" do
          let(:expansions) { true }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "status" => { "held" => false,
                            "hold_expiry" => nil,
                            "last_delivery_attempt" => nil,
                            "status" => "Pending" },
              "details" => { "bounce" => false,
                             "bounce_for_id" => 0,
                             "direction" => "outgoing",
                             "mail_from" => "[email protected]",
                             "message_id" => message.message_id,
                             "rcpt_to" => "[email protected]",
                             "received_with_ssl" => nil,
                             "size" => kind_of(String),
                             "subject" => "An example message",
                             "tag" => nil,
                             "timestamp" => kind_of(Float) },
              "inspection" => { "inspected" => false,
                                "spam" => false,
                                "spam_score" => 0.0,
                                "threat" => false,
                                "threat_details" => nil },
              "plain_body" => message.plain_body,
              "html_body" => message.html_body,
              "attachments" => [],
              "headers" => message.headers,
              "raw_message" => Base64.encode64(message.raw_message),
              "activity_entries" => {
                "loads" => [],
                "clicks" => []
              }
            })
          end
        end

        context "when the status expansion is requested" do
          let(:expansions) { ["status"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "status" => { "held" => false,
                            "hold_expiry" => nil,
                            "last_delivery_attempt" => nil,
                            "status" => "Pending" }
            })
          end
        end

        context "when the details expansion is requested" do
          let(:expansions) { ["details"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "details" => { "bounce" => false,
                             "bounce_for_id" => 0,
                             "direction" => "outgoing",
                             "mail_from" => "[email protected]",
                             "message_id" => message.message_id,
                             "rcpt_to" => "[email protected]",
                             "received_with_ssl" => nil,
                             "size" => kind_of(String),
                             "subject" => "An example message",
                             "tag" => nil,
                             "timestamp" => kind_of(Float) }
            })
          end
        end

        context "when the details expansion is requested" do
          let(:expansions) { ["inspection"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "inspection" => { "inspected" => false,
                                "spam" => false,
                                "spam_score" => 0.0,
                                "threat" => false,
                                "threat_details" => nil }
            })
          end
        end

        context "when the body expansions are requested" do
          let(:expansions) { %w[plain_body html_body] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "plain_body" => message.plain_body,
              "html_body" => message.html_body
            })
          end
        end

        context "when the attachments expansions is requested" do
          let(:message) do
            MessageFactory.outgoing(server) do |_, mail|
              mail.attachments["example.txt"] = "hello world!"
            end
          end
          let(:expansions) { ["attachments"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "attachments" => [
                {
                  "content_type" => "text/plain",
                  "data" => Base64.encode64("hello world!"),
                  "filename" => "example.txt",
                  "hash" => Digest::SHA1.hexdigest("hello world!"),
                  "size" => 12
                },
              ]
            })
          end
        end

        context "when the headers expansions is requested" do
          let(:expansions) { ["headers"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "headers" => message.headers
            })
          end
        end

        context "when the raw_message expansions is requested" do
          let(:expansions) { ["raw_message"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "raw_message" => Base64.encode64(message.raw_message)
            })
          end
        end

        context "when the activity_entries expansions is requested" do
          let(:message) do
            MessageFactory.outgoing(server) do |msg|
              msg.create_load(double("request", ip: "1.2.3.4", user_agent: "user agent"))
              link = msg.create_link("https://example.com")
              link_id = msg.database.select(:links, where: { token: link }).first["id"]
              msg.database.insert(:clicks, {
                message_id: msg.id,
                link_id: link_id,
                ip_address: "1.2.3.4",
                user_agent: "user agent",
                timestamp: Time.now.to_f
              })
            end
          end
          let(:expansions) { ["activity_entries"] }

          it "returns details about the message" do
            expect(response.status).to eq 200
            parsed_body = JSON.parse(response.body)
            expect(parsed_body["status"]).to eq "success"
            expect(parsed_body["data"]).to match({
              "id" => message.id,
              "token" => message.token,
              "activity_entries" => {
                "loads" => [{
                  "ip_address" => "1.2.3.4",
                  "user_agent" => "user agent",
                  "timestamp" => match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\z/)
                }],
                "clicks" => [{
                  "url" => "https://example.com",
                  "ip_address" => "1.2.3.4",
                  "user_agent" => "user agent",
                  "timestamp" => match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\z/)
                }]
              }
            })
          end
        end
      end
    end
  end
end