spec/models/tenant_spec.rb
require "rails_helper"
require "active_storage/service/disk_service"
describe Tenant do
describe ".resolve_host" do
before do
allow(Tenant).to receive(:default_url_options).and_return({ host: "consul.dev" })
end
it "returns nil for empty hosts" do
expect(Tenant.resolve_host("")).to be nil
expect(Tenant.resolve_host(nil)).to be nil
end
it "returns nil for IP addresses" do
expect(Tenant.resolve_host("127.0.0.1")).to be nil
end
it "returns nil using development and test domains" do
expect(Tenant.resolve_host("localhost")).to be nil
expect(Tenant.resolve_host("lvh.me")).to be nil
expect(Tenant.resolve_host("example.com")).to be nil
expect(Tenant.resolve_host("www.example.com")).to be nil
end
it "treats lvh.me as localhost" do
expect(Tenant.resolve_host("jupiter.lvh.me")).to eq "jupiter"
expect(Tenant.resolve_host("www.lvh.me")).to be nil
end
it "returns nil for the default host" do
expect(Tenant.resolve_host("consul.dev")).to be nil
end
it "ignores the www prefix" do
expect(Tenant.resolve_host("www.consul.dev")).to be nil
end
it "returns subdomains when present" do
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
end
it "ignores the www prefix when subdomains are present" do
expect(Tenant.resolve_host("www.saturn.consul.dev")).to eq "saturn"
end
it "returns nested additional subdomains" do
expect(Tenant.resolve_host("europa.jupiter.consul.dev")).to eq "europa.jupiter"
end
it "ignores the www prefix in additional nested subdomains" do
expect(Tenant.resolve_host("www.europa.jupiter.consul.dev")).to eq "europa.jupiter"
end
it "does not ignore www if it isn't the prefix" do
expect(Tenant.resolve_host("wwwsaturn.consul.dev")).to eq "wwwsaturn"
expect(Tenant.resolve_host("saturn.www.consul.dev")).to eq "saturn.www"
end
it "returns the host as a subdomain" do
expect(Tenant.resolve_host("consul.dev.consul.dev")).to eq "consul.dev"
end
it "returns nested subdomains containing the host" do
expect(Tenant.resolve_host("saturn.consul.dev.consul.dev")).to eq "saturn.consul.dev"
end
it "returns full domains when they don't contain the host" do
expect(Tenant.resolve_host("unrelated.dev")).to eq "unrelated.dev"
expect(Tenant.resolve_host("mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
end
it "ignores the www prefix in full domains" do
expect(Tenant.resolve_host("www.unrelated.dev")).to eq "unrelated.dev"
expect(Tenant.resolve_host("www.mercury.anotherconsul.dev")).to eq "mercury.anotherconsul.dev"
end
it "returns full domains when there's a tenant with a domain including the host" do
insert(:tenant, :domain, schema: "saturn.consul.dev")
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn.consul.dev"
end
it "returns subdomains when there's a subdomain-type tenant with that domain" do
insert(:tenant, schema: "saturn.consul.dev")
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn"
end
it "raises an exception when a domain is accessed as a subdomain" do
insert(:tenant, :domain, schema: "saturn.dev")
expect { Tenant.resolve_host("saturn.dev.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "returns nested subdomains when there's a subdomain-type tenant with nested subdomains" do
insert(:tenant, schema: "saturn.dev")
expect(Tenant.resolve_host("saturn.dev.consul.dev")).to eq "saturn.dev"
end
it "returns domains when there are two tenants resolving to the same domain" do
insert(:tenant, schema: "saturn")
insert(:tenant, :domain, schema: "saturn.consul.dev")
expect(Tenant.resolve_host("saturn.consul.dev")).to eq "saturn.consul.dev"
end
it "returns domains when there's a tenant using the default host" do
insert(:tenant, :domain, schema: "consul.dev")
expect(Tenant.resolve_host("consul.dev")).to eq "consul.dev"
end
it "returns domains including www when the tenant contains it" do
insert(:tenant, :domain, schema: "www.consul.dev")
expect(Tenant.resolve_host("www.consul.dev")).to eq "www.consul.dev"
end
it "raises an exception when accessing a hidden tenant using a subdomain" do
insert(:tenant, schema: "saturn", hidden_at: Time.current)
expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "raises an exception when accessing a hidden tenant using a domain" do
insert(:tenant, :domain, schema: "consul.dev", hidden_at: Time.current)
expect { Tenant.resolve_host("consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "raises an exception when accessing a hidden tenant using a domain starting with www" do
insert(:tenant, :domain, schema: "www.consul.dev", hidden_at: Time.current)
expect { Tenant.resolve_host("www.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "raises an exception with a hidden tenant's domain when another tenant resolves to the same domain" do
insert(:tenant, :domain, schema: "saturn.consul.dev", hidden_at: Time.current)
insert(:tenant, schema: "saturn")
expect { Tenant.resolve_host("saturn.consul.dev") }.to raise_exception(Apartment::TenantNotFound)
end
it "ignores hidden tenants with nil as their schema" do
insert(:tenant, schema: nil, hidden_at: Time.current)
expect(Tenant.resolve_host("consul.dev")).to be nil
end
context "multitenancy disabled" do
before { allow(Rails.application.config).to receive(:multitenancy).and_return(false) }
it "always returns nil" do
expect(Tenant.resolve_host("saturn.consul.dev")).to be nil
expect(Tenant.resolve_host("jupiter.lvh.me")).to be nil
end
end
context "default host contains subdomains" do
before do
allow(Tenant).to receive(:default_url_options).and_return({ host: "demo.consul.dev" })
end
it "ignores subdomains already present in the default host" do
expect(Tenant.resolve_host("demo.consul.dev")).to be nil
end
it "ignores the www prefix" do
expect(Tenant.resolve_host("www.demo.consul.dev")).to be nil
end
it "returns additional subdomains" do
expect(Tenant.resolve_host("saturn.demo.consul.dev")).to eq "saturn"
end
it "ignores the www prefix in additional subdomains" do
expect(Tenant.resolve_host("www.saturn.demo.consul.dev")).to eq "saturn"
end
it "returns nested additional subdomains" do
expect(Tenant.resolve_host("europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
end
it "ignores the www prefix in additional nested subdomains" do
expect(Tenant.resolve_host("www.europa.jupiter.demo.consul.dev")).to eq "europa.jupiter"
end
it "does not ignore www if it isn't the prefix" do
expect(Tenant.resolve_host("wwwsaturn.demo.consul.dev")).to eq "wwwsaturn"
expect(Tenant.resolve_host("saturn.www.demo.consul.dev")).to eq "saturn.www"
end
end
context "default host is similar to development and test domains" do
before do
allow(Tenant).to receive(:default_url_options).and_return({ host: "mylvh.me" })
end
it "returns nil for the default host" do
expect(Tenant.resolve_host("mylvh.me")).to be nil
end
it "returns subdomains when present" do
expect(Tenant.resolve_host("neptune.mylvh.me")).to eq "neptune"
end
end
end
describe ".host_for" do
before do
allow(Tenant).to receive(:default_url_options).and_return({ host: "consul.dev" })
end
it "returns the default host for the default schema" do
expect(Tenant.host_for("public")).to eq "consul.dev"
end
it "returns the host with a subdomain on other schemas" do
expect(Tenant.host_for("uranus")).to eq "uranus.consul.dev"
end
it "uses lvh.me for subdomains when the host is localhost" do
allow(Tenant).to receive(:default_url_options).and_return({ host: "localhost" })
expect(Tenant.host_for("uranus")).to eq "uranus.lvh.me"
end
it "ignores the default host when given a full domain" do
insert(:tenant, :domain, schema: "whole.galaxy")
expect(Tenant.host_for("whole.galaxy")).to eq "whole.galaxy"
end
it "uses the default host when given nested subdomains" do
insert(:tenant, schema: "whole.galaxy")
expect(Tenant.host_for("whole.galaxy")).to eq "whole.galaxy.consul.dev"
end
end
describe ".current_secrets" do
context "same secrets for all tenants" do
before do
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
star: "Sun",
volume: "Medium"
))
end
it "returns the default secrets for the default tenant" do
allow(Tenant).to receive(:current_schema).and_return("public")
expect(Tenant.current_secrets.star).to eq "Sun"
expect(Tenant.current_secrets.volume).to eq "Medium"
end
it "returns the default secrets for other tenants" do
allow(Tenant).to receive(:current_schema).and_return("earth")
expect(Tenant.current_secrets.star).to eq "Sun"
expect(Tenant.current_secrets.volume).to eq "Medium"
end
end
context "tenant overwriting secrets" do
before do
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
star: "Sun",
volume: "Medium",
tenants: { proxima: { star: "Alpha Centauri" }}
))
end
it "returns the default secrets for the default tenant" do
allow(Tenant).to receive(:current_schema).and_return("public")
expect(Tenant.current_secrets.star).to eq "Sun"
expect(Tenant.current_secrets.volume).to eq "Medium"
end
it "returns the overwritten secrets for tenants overwriting them" do
allow(Tenant).to receive(:current_schema).and_return("proxima")
expect(Tenant.current_secrets.star).to eq "Alpha Centauri"
expect(Tenant.current_secrets.volume).to eq "Medium"
end
it "returns the default secrets for other tenants" do
allow(Tenant).to receive(:current_schema).and_return("earth")
expect(Tenant.current_secrets.star).to eq "Sun"
expect(Tenant.current_secrets.volume).to eq "Medium"
end
end
end
describe ".run_on_each" do
it "runs the code on all tenants, including the default one" do
create(:tenant, schema: "andromeda")
create(:tenant, schema: "milky-way")
Tenant.run_on_each do
Setting["org_name"] = "oh-my-#{Tenant.current_schema}"
end
expect(Setting["org_name"]).to eq "oh-my-public"
Tenant.switch("andromeda") do
expect(Setting["org_name"]).to eq "oh-my-andromeda"
end
Tenant.switch("milky-way") do
expect(Setting["org_name"]).to eq "oh-my-milky-way"
end
end
end
describe "scopes" do
describe ".domain" do
it "returns tenants with domain schema type" do
insert(:tenant, schema_type: :domain, schema: "full.domain")
expect(Tenant.domain.pluck(:schema)).to eq ["full.domain"]
end
it "does not return tenants with subdomain schema type" do
insert(:tenant, schema_type: :subdomain, schema: "nested.subdomain")
expect(Tenant.domain).to be_empty
end
end
end
describe "validations" do
let(:tenant) { build(:tenant) }
it "is valid" do
expect(tenant).to be_valid
end
it "is not valid without a schema" do
tenant.schema = nil
expect(tenant).not_to be_valid
end
it "is not valid with an already existing schema" do
insert(:tenant, schema: "subdomainx")
expect(build(:tenant, schema: "subdomainx")).not_to be_valid
end
it "is not valid with the schema of an already existing hidden record" do
insert(:tenant, schema: "subdomainx", hidden_at: Time.current)
expect(build(:tenant, schema: "subdomainx")).not_to be_valid
end
it "is not valid with an excluded subdomain" do
%w[mail public shared_extensions www].each do |subdomain|
tenant.schema = subdomain
expect(tenant).not_to be_valid
end
end
it "is valid with nested subdomains" do
tenant.schema = "multiple.sub.domains"
expect(tenant).to be_valid
end
it "is not valid with an invalid subdomain" do
tenant.schema = "my sub domain"
expect(tenant).not_to be_valid
end
it "is not valid without a name" do
tenant.name = ""
expect(tenant).not_to be_valid
end
it "is not valid with an already existing name" do
insert(:tenant, name: "Name X")
expect(build(:tenant, name: "Name X")).not_to be_valid
end
it "is not valid with the name of an already existing hidden record" do
insert(:tenant, name: "Name X", hidden_at: Time.current)
expect(build(:tenant, name: "Name X")).not_to be_valid
end
context "Domain schema type" do
before { tenant.schema_type = :domain }
it "is valid with domains" do
tenant.schema = "my.domain"
expect(tenant).to be_valid
end
it "is valid with domains which are machine names" do
tenant.schema = "localmachine"
expect(tenant).to be_valid
end
end
end
describe "#create_schema" do
it "creates a schema creating a record" do
create(:tenant, schema: "new")
expect { Tenant.switch("new") { nil } }.not_to raise_exception
end
end
describe "#rename_schema" do
it "renames the schema when updating the schema" do
tenant = create(:tenant, schema: "typo")
tenant.update!(schema: "notypo")
expect { Tenant.switch("typo") { nil } }.to raise_exception(Apartment::TenantNotFound)
expect { Tenant.switch("notypo") { nil } }.not_to raise_exception
end
end
describe "#rename_storage" do
after do
FileUtils.rm_rf(File.join(ActiveStorage::Blob.service.root, "tenants", "notypo"))
end
it "does nothing when the active storage blob service cannot manage tenants" do
allow(Rails.configuration.active_storage).to receive(:service_configurations) do
ActiveSupport::ConfigurationFile.parse(Rails.root.join("config/storage.yml")).tap do |config|
config[Rails.configuration.active_storage.service.to_s]["service"] = "Disk"
end
end
tenant = create(:tenant, schema: "typo")
expect(File).not_to receive(:rename)
tenant.update!(schema: "notypo")
end
it "does nothing when the tenant has no files to move" do
tenant = create(:tenant, schema: "typo")
expect(File).not_to receive(:rename)
tenant.update!(schema: "notypo")
end
it "renames the active storage folder when updating the schema" do
tenant = create(:tenant, schema: "typo")
Tenant.switch("typo") do
Setting.reset_defaults
create(:image)
end
expect(File).to receive(:rename).and_call_original
tenant.update!(schema: "notypo")
Tenant.switch("notypo") do
image = Image.first
expect(image.file_path).to include "/notypo/"
expect(File.exist?(image.file_path)).to be true
end
end
end
describe "#destroy_schema" do
it "drops the schema when destroying a record" do
tenant = create(:tenant, schema: "wrong")
tenant.destroy!
expect { Tenant.switch("wrong") { nil } }.to raise_exception(Apartment::TenantNotFound)
end
end
end