From 3d208d632f4fc8a4adbfdb2bf4b377271eae6692 Mon Sep 17 00:00:00 2001 From: Adam Cooke Date: Wed, 13 Mar 2024 18:18:14 +0000 Subject: [PATCH] test: add tests for the legacy API --- app/models/outgoing_message_prototype.rb | 2 +- .../legacy_api/messages/deliveries_spec.rb | 118 ++++++++ spec/apis/legacy_api/messages/message_spec.rb | 270 ++++++++++++++++++ spec/apis/legacy_api/send/message_spec.rb | 234 +++++++++++++++ spec/apis/legacy_api/send/raw_spec.rb | 152 ++++++++++ spec/rails_helper.rb | 11 +- 6 files changed, 784 insertions(+), 3 deletions(-) create mode 100644 spec/apis/legacy_api/messages/deliveries_spec.rb create mode 100644 spec/apis/legacy_api/messages/message_spec.rb create mode 100644 spec/apis/legacy_api/send/message_spec.rb create mode 100644 spec/apis/legacy_api/send/raw_spec.rb diff --git a/app/models/outgoing_message_prototype.rb b/app/models/outgoing_message_prototype.rb index 276c180..8584b72 100644 --- a/app/models/outgoing_message_prototype.rb +++ b/app/models/outgoing_message_prototype.rb @@ -99,7 +99,7 @@ class OutgoingMessagePrototype { name: attachment[:name], content_type: attachment[:content_type] || "application/octet-stream", - data: attachment[:base64] ? Base64.decode64(attachment[:data]) : attachment[:data] + data: attachment[:base64] && attachment[:data] ? Base64.decode64(attachment[:data]) : attachment[:data] } end end diff --git a/spec/apis/legacy_api/messages/deliveries_spec.rb b/spec/apis/legacy_api/messages/deliveries_spec.rb new file mode 100644 index 0000000..2daf95f --- /dev/null +++ b/spec/apis/legacy_api/messages/deliveries_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Legacy Messages API", type: :request do + describe "/api/v1/messages/deliveries" do + context "when no authentication is provided" do + it "returns an error" do + post "/api/v1/messages/deliveries" + 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/deliveries", 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/deliveries", 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/deliveries", 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/deliveries", + 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" + expect(parsed_body["data"]["id"]).to eq 123 + end + end + + context "when the message ID exists" do + let(:server) { create(:server) } + let(:credential) { create(:credential, server: server) } + let(:message) { MessageFactory.outgoing(server) } + + before do + message.create_delivery("SoftFail", details: "no server found", + output: "404", + sent_with_ssl: true, + log_id: "1234", + time: 1.2) + message.create_delivery("Sent", details: "sent successfully", + output: "200", + sent_with_ssl: false, + log_id: "5678", + time: 2.2) + end + + before do + post "/api/v1/messages/deliveries", + headers: { "x-server-api-key" => credential.key, + "content-type" => "application/json" }, + params: { id: message.id }.to_json + end + + it "returns an array of deliveries" 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" => kind_of(Integer), + "status" => "SoftFail", + "details" => "no server found", + "output" => "404", + "sent_with_ssl" => true, + "log_id" => "1234", + "time" => 1.2, + "timestamp" => kind_of(Float) }, + { "id" => kind_of(Integer), + "status" => "Sent", + "details" => "sent successfully", + "output" => "200", + "sent_with_ssl" => false, + "log_id" => "5678", + "time" => 2.2, + "timestamp" => kind_of(Float) }, + ]) + end + end + end + end +end diff --git a/spec/apis/legacy_api/messages/message_spec.rb b/spec/apis/legacy_api/messages/message_spec.rb new file mode 100644 index 0000000..4140f9a --- /dev/null +++ b/spec/apis/legacy_api/messages/message_spec.rb @@ -0,0 +1,270 @@ +# 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 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" => "test@example.com", + "message_id" => message.message_id, + "rcpt_to" => "john@example.com", + "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 diff --git a/spec/apis/legacy_api/send/message_spec.rb b/spec/apis/legacy_api/send/message_spec.rb new file mode 100644 index 0000000..da3855c --- /dev/null +++ b/spec/apis/legacy_api/send/message_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Legacy Send API", type: :request do + describe "/api/v1/send/message" do + context "when no authentication is provided" do + it "returns an error" do + post "/api/v1/send/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/send/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/send/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) } + let(:domain) { create(:domain, owner: server) } + let(:default_params) do + { + to: ["test@example.com"], + cc: ["cc@example.com"], + bcc: ["bcc@example.com"], + from: "test@#{domain.name}", + sender: "sender@#{domain.name}", + tag: "test-tag", + reply_to: "reply@example.com", + plain_body: "plain text", + html_body: "

html

", + attachments: [{ name: "test1.txt", content_type: "text/plain", data: Base64.encode64("hello world 1") }, + { name: "test2.txt", content_type: "text/plain", data: Base64.encode64("hello world 2") },], + headers: { "x-test-header-1" => "111", "x-test-header-2" => "222" }, + bounce: false, + subject: "Test" + } + end + let(:params) { default_params } + + before do + post "/api/v1/send/message", + headers: { "x-server-api-key" => credential.key, + "content-type" => "application/json" }, + params: params.to_json + end + + context "when no recipients are provided" do + let(:params) { default_params.merge(to: [], cc: [], bcc: []) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "NoRecipients" + expect(parsed_body["data"]["message"]).to match(/there are no recipients defined to receive this message/i) + end + end + + context "when no content is provided" do + let(:params) { default_params.merge(html_body: nil, plain_body: nil) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "NoContent" + expect(parsed_body["data"]["message"]).to match(/there is no content defined for this e-mail/i) + end + end + + context "when the number of 'To' recipients exceeds the maximum" do + let(:params) { default_params.merge(to: ["a@a.com"] * 51) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyToAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of To addresses has been reached/i) + end + end + + context "when the number of 'CC' recipients exceeds the maximum" do + let(:params) { default_params.merge(cc: ["a@a.com"] * 51) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyCCAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of CC addresses has been reached/i) + end + end + + context "when the number of 'BCC' recipients exceeds the maximum" do + let(:params) { default_params.merge(bcc: ["a@a.com"] * 51) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "TooManyBCCAddresses" + expect(parsed_body["data"]["message"]).to match(/the maximum number of BCC addresses has been reached/i) + end + end + + context "when the 'From' address is missing" do + let(:params) { default_params.merge(from: nil) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "FromAddressMissing" + expect(parsed_body["data"]["message"]).to match(/the from address is missing and is required/i) + end + end + + context "when the 'From' address is not authorised" do + let(:params) { default_params.merge(from: "test@another.com") } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "UnauthenticatedFromAddress" + expect(parsed_body["data"]["message"]).to match(/the from address is not authorised to send mail from this server/i) + end + end + + context "when an attachment is missing a name" do + let(:params) { default_params.merge(attachments: [{ name: nil, content_type: "text/plain", data: Base64.encode64("hello world 1") }]) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "AttachmentMissingName" + expect(parsed_body["data"]["message"]).to match(/an attachment is missing a name/i) + end + end + + context "when an attachment is missing data" do + let(:params) { default_params.merge(attachments: [{ name: "test1.txt", content_type: "text/plain", data: nil }]) } + + it "returns an error" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "error" + expect(parsed_body["data"]["code"]).to eq "AttachmentMissingData" + expect(parsed_body["data"]["message"]).to match(/an attachment is missing data/i) + end + end + + context "when an attachment entry is not a hash" do + let(:params) { default_params.merge(attachments: [123, "string"]) } + + it "continues as if it wasn't there" do + parsed_body = JSON.parse(response.body) + ["test@example.com", "cc@example.com", "bcc@example.com"].each do |rcpt_to| + message_id = parsed_body["data"]["messages"][rcpt_to]["id"] + message = server.message(message_id) + expect(message.attachments).to be_empty + end + end + end + + context "when given a complete email to send" do + it "returns details of the messages created" do + parsed_body = JSON.parse(response.body) + expect(parsed_body["status"]).to eq "success" + expect(parsed_body["data"]["messages"]).to match({ + "test@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, + "cc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, + "bcc@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ } + }) + end + + it "adds an appropriate received header" do + parsed_body = JSON.parse(response.body) + message_id = parsed_body["data"]["messages"]["test@example.com"]["id"] + message = server.message(message_id) + expect(message.headers["received"].first).to match(/\Afrom api/) + end + + it "creates appropriate message objects" do + parsed_body = JSON.parse(response.body) + ["test@example.com", "cc@example.com", "bcc@example.com"].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: params[:from], + subject: params[:subject], + message_id: kind_of(String), + timestamp: kind_of(Time), + domain_id: domain.id, + credential_id: credential.id, + bounce: false, + tag: params[:tag], + headers: hash_including("x-test-header-1" => ["111"], + "x-test-header-2" => ["222"], + "sender" => [params[:sender]], + "to" => ["test@example.com"], + "cc" => ["cc@example.com"], + "reply-to" => ["reply@example.com"]), + plain_body: params[:plain_body], + html_body: params[:html_body], + attachments: [ + have_attributes(content_type: /\Atext\/plain/, filename: "test1.txt", body: have_attributes(to_s: "hello world 1")), + have_attributes(content_type: /\Atext\/plain/, filename: "test2.txt", body: have_attributes(to_s: "hello world 2")), + ] + ) + end + end + end + end + end +end diff --git a/spec/apis/legacy_api/send/raw_spec.rb b/spec/apis/legacy_api/send/raw_spec.rb new file mode 100644 index 0000000..795677f --- /dev/null +++ b/spec/apis/legacy_api/send/raw_spec.rb @@ -0,0 +1,152 @@ +# 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 = "test1@example.com" + 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 = "

html

" + mail + end + let(:default_params) do + { + mail_from: "test@#{domain.name}", + rcpt_to: ["test1@example.com", "test2@example.com"], + data: Base64.encode64(data.to_s), + bounce: false + } + end + let(:params) { default_params } + + before do + post "/api/v1/send/raw", + headers: { "x-server-api-key" => credential.key, + "content-type" => "application/json" }, + params: params.to_json + 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({ + "test1@example.com" => { "id" => kind_of(Integer), "token" => /\A[a-zA-Z0-9]{16}\z/ }, + "test2@example.com" => { "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) + ["test1@example.com", "test2@example.com"].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" => ["test1@example.com"]), + plain_body: "plain text", + html_body: "

html

", + attachments: [], + received_with_ssl: true, + scope: "outgoing", + raw_message: data.to_s + ) + end + end + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e1df6a0..526651e 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -37,9 +37,16 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods config.include GeneralHelpers + # Before all request specs, set the hostname to the web hostname for + # Postal otherwise it'll be www.example.com which will fail host + # authorization checks. + config.before(:each, type: :request) do + host! Postal::Config.postal.web_hostname + end + + # Test that the factories are working as they should and then clean up before getting started on + # the rest of the suite. config.before(:suite) do - # Test that the factories are working as they should and then clean up before getting started on - # the rest of the suite. DatabaseCleaner.start FactoryBot.lint ensure