مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-11-30 21:32:30 +00:00
test: add tests for Server model
هذا الالتزام موجود في:
1
Gemfile
1
Gemfile
@@ -52,6 +52,7 @@ group :development do
|
||||
gem "rspec-rails", require: false
|
||||
gem "rubocop"
|
||||
gem "rubocop-rails"
|
||||
gem "shoulda-matchers"
|
||||
gem "timecop"
|
||||
gem "webmock"
|
||||
end
|
||||
|
||||
@@ -289,6 +289,8 @@ GEM
|
||||
sentry-ruby (~> 5.8.0)
|
||||
sentry-ruby (5.8.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
shoulda-matchers (6.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sprockets (4.2.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
@@ -366,6 +368,7 @@ DEPENDENCIES
|
||||
secure_headers
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
shoulda-matchers
|
||||
timecop
|
||||
turbolinks (~> 5)
|
||||
uglifier (>= 1.3.0)
|
||||
|
||||
@@ -192,15 +192,22 @@ class Server < ApplicationRecord
|
||||
end
|
||||
|
||||
def send_limit_approaching?
|
||||
send_limit && (send_volume >= send_limit * 0.90)
|
||||
return false unless send_limit
|
||||
|
||||
(send_volume >= send_limit * 0.90)
|
||||
end
|
||||
|
||||
def send_limit_exceeded?
|
||||
send_limit && send_volume >= send_limit
|
||||
return false unless send_limit
|
||||
|
||||
send_volume >= send_limit
|
||||
end
|
||||
|
||||
def send_limit_warning(type)
|
||||
if organization.notification_addresses.present?
|
||||
AppMailer.send("server_send_limit_#{type}", self).deliver
|
||||
end
|
||||
|
||||
update_column("send_limit_#{type}_notified_at", Time.now)
|
||||
WebhookRequest.trigger(self, "SendLimit#{type.to_s.capitalize}", server: webhook_hash, volume: send_volume, limit: send_limit)
|
||||
end
|
||||
@@ -209,17 +216,6 @@ class Server < ApplicationRecord
|
||||
@queue_size ||= queued_messages.ready.count
|
||||
end
|
||||
|
||||
def stats
|
||||
{
|
||||
queue: queue_size,
|
||||
held: held_messages,
|
||||
bounce_rate: bounce_rate,
|
||||
message_rate: message_rate,
|
||||
throughput: throughput_stats,
|
||||
size: message_db.total_size
|
||||
}
|
||||
end
|
||||
|
||||
# Return the domain which can be used to authenticate emails sent from the given e-mail address.
|
||||
#
|
||||
# @param address [String] an e-mail address
|
||||
@@ -274,8 +270,11 @@ class Server < ApplicationRecord
|
||||
self.suspended_at = Time.now
|
||||
self.suspension_reason = reason
|
||||
save!
|
||||
if organization.notification_addresses.present?
|
||||
AppMailer.server_suspended(self).deliver
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def unsuspend
|
||||
self.suspended_at = nil
|
||||
@@ -283,12 +282,6 @@ class Server < ApplicationRecord
|
||||
save!
|
||||
end
|
||||
|
||||
def validate_ip_pool_belongs_to_organization
|
||||
return unless ip_pool && ip_pool_id_changed? && !organization.ip_pools.include?(ip_pool)
|
||||
|
||||
errors.add :ip_pool_id, "must belong to the organization"
|
||||
end
|
||||
|
||||
def ip_pool_for_message(message)
|
||||
return unless message.scope == "outgoing"
|
||||
|
||||
@@ -300,15 +293,26 @@ class Server < ApplicationRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ip_pool
|
||||
end
|
||||
|
||||
def self.triggered_send_limit(type)
|
||||
private
|
||||
|
||||
def validate_ip_pool_belongs_to_organization
|
||||
return unless ip_pool && ip_pool_id_changed? && !organization.ip_pools.include?(ip_pool)
|
||||
|
||||
errors.add :ip_pool_id, "must belong to the organization"
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
def triggered_send_limit(type)
|
||||
servers = where("send_limit_#{type}_at IS NOT NULL AND send_limit_#{type}_at > ?", 3.minutes.ago)
|
||||
servers.where("send_limit_#{type}_notified_at IS NULL OR send_limit_#{type}_notified_at < ?", 1.hour.ago)
|
||||
end
|
||||
|
||||
def self.send_send_limit_notifications
|
||||
def send_send_limit_notifications
|
||||
[:approaching, :exceeded].each_with_object({}) do |type, hash|
|
||||
hash[type] = 0
|
||||
servers = triggered_send_limit(type)
|
||||
@@ -321,25 +325,16 @@ class Server < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def self.[](id, extra = nil)
|
||||
server = nil
|
||||
if id.is_a?(String)
|
||||
if id =~ /\A(\w+)\/(\w+)\z/
|
||||
server = includes(:organization).where(organizations: { permalink: ::Regexp.last_match(1) }, permalink: ::Regexp.last_match(2)).first
|
||||
end
|
||||
def [](id, extra = nil)
|
||||
if id.is_a?(String) && id =~ /\A(\w+)\/(\w+)\z/
|
||||
joins(:organization).where(
|
||||
organizations: { permalink: ::Regexp.last_match(1) }, permalink: ::Regexp.last_match(2)
|
||||
).first
|
||||
else
|
||||
server = where(id: id).first
|
||||
find_by(id: id.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
if extra
|
||||
if extra.is_a?(String)
|
||||
server.domains.where(name: extra.to_s).first
|
||||
else
|
||||
server.message(extra.to_i)
|
||||
end
|
||||
else
|
||||
server
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
require "postal/config"
|
||||
|
||||
if Postal.config&.smtp
|
||||
# TODO: by default, we should just send mail through the local Postal
|
||||
# installation rather than having to actually configure an SMTP server.
|
||||
ActionMailer::Base.delivery_method = :smtp
|
||||
ActionMailer::Base.smtp_settings = { address: Postal.config.smtp.host, user_name: Postal.config.smtp.username, password: Postal.config.smtp.password, port: Postal.config.smtp.port || 25 }
|
||||
end
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe Server do
|
||||
context "model" do
|
||||
subject(:server) { create(:server) }
|
||||
|
||||
it "should have a UUID" do
|
||||
expect(server.uuid).to be_a String
|
||||
expect(server.uuid.length).to eq 36
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -42,6 +42,17 @@ FactoryBot.define do
|
||||
sequence(:name) { |n| "example#{n}.com" }
|
||||
verification_method { "DNS" }
|
||||
verified_at { Time.now }
|
||||
|
||||
trait :unverified do
|
||||
verified_at { nil }
|
||||
end
|
||||
|
||||
trait :dns_all_ok do
|
||||
spf_status { "OK" }
|
||||
dkim_status { "OK" }
|
||||
mx_status { "OK" }
|
||||
return_path_status { "OK" }
|
||||
end
|
||||
end
|
||||
|
||||
factory :organization_domain, parent: :domain do
|
||||
|
||||
15
spec/factories/ip_pool_rule_factory.rb
Normal file
15
spec/factories/ip_pool_rule_factory.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :ip_pool_rule do
|
||||
owner factory: :organization
|
||||
ip_pool
|
||||
to_text { "google.com" }
|
||||
|
||||
after(:build) do |ip_pool_rule|
|
||||
if ip_pool_rule.ip_pool.organizations.empty? && ip_pool_rule.owner.is_a?(Organization)
|
||||
ip_pool_rule.ip_pool.organizations << ip_pool_rule.owner
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -28,5 +28,10 @@ FactoryBot.define do
|
||||
name { "Acme Inc" }
|
||||
sequence(:permalink) { |n| "org#{n}" }
|
||||
association :owner, factory: :user
|
||||
|
||||
trait :suspended do
|
||||
suspended_at { 1.day.ago }
|
||||
suspension_reason { "test" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,6 +52,7 @@ FactoryBot.define do
|
||||
|
||||
trait :suspended do
|
||||
suspended_at { Time.current }
|
||||
suspension_reason { "Test Reason" }
|
||||
end
|
||||
|
||||
trait :exceeded_send_limit do
|
||||
|
||||
614
spec/models/server_spec.rb
Normal file
614
spec/models/server_spec.rb
Normal file
@@ -0,0 +1,614 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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("hello@mangos.io")).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("hello@oranges.io")).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("hello@oranges.io")).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("hello@lemons.com")).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("hello@bananas.com")).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("hello@bananas.com")).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" => "test@lemons.com")).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" => ["hello@lemons.com", "hello@apples.com"])).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}", "hello@lemons.com"])).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" => "hello@lemons.com",
|
||||
"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" => "hello@lemons.com",
|
||||
"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 = "hello@google.com"
|
||||
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 = "hello@google.com"
|
||||
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
|
||||
@@ -9,6 +9,7 @@ require "factory_bot"
|
||||
require "timecop"
|
||||
require "database_cleaner"
|
||||
require "webmock/rspec"
|
||||
require "shoulda-matchers"
|
||||
|
||||
DatabaseCleaner.allow_remote_database_url = true
|
||||
ActiveRecord::Base.logger = Logger.new("/dev/null")
|
||||
@@ -16,8 +17,17 @@ ActiveRecord::Base.logger = Logger.new("/dev/null")
|
||||
Dir[File.expand_path("factories/*.rb", __dir__)].each { |f| require f }
|
||||
Dir[File.expand_path("helpers/**/*.rb", __dir__)].each { |f| require f }
|
||||
|
||||
ActionMailer::Base.delivery_method = :test
|
||||
|
||||
ActiveRecord::Migration.maintain_test_schema!
|
||||
|
||||
Shoulda::Matchers.configure do |config|
|
||||
config.integrate do |with|
|
||||
with.test_framework :rspec
|
||||
with.library :rails
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.use_transactional_fixtures = true
|
||||
config.infer_spec_type_from_file_location!
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم