Files
grecia/spec/models/tenant_spec.rb
Javi Martín 118c2bf5e0 Move custom ActiveStorage service to $LOAD_PATH
We moved this file to `app/lib/` in commit cb477149c so it would be in
the autoload_paths. However, this class is loaded by ActiveStorage, with
the following method:

```
def resolve(class_name)
  require "active_storage/service/#{class_name.to_s.underscore}_service"
  ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
rescue LoadError
  raise "Missing service adapter for #{class_name.inspect}"
end
``

So this file needs to be in the $LOAD_PATH, or else ActiveStorage won't
be able to load it when we disable the `add_autoload_paths_to_load_path`
option, which is the default in Rails 7.1 [1].

Moving it to the `lib` folder solves the issue; as mentioned in the
guide to upgrade to Rails 7.1 [2]:

> The lib directory is not affected by this flag, it is added to
> $LOAD_PATH always.

However, we were also referencing this class in the `Tenant` model,
meaning we needed to autoload it as well somehow. So, instead of
directly referencing this class, we're using `respond_to?` in the Tenant
model.

We're changing the test so it fails when the code calls
`is_a?(ActiveStorage::Service::TenantDiskService)`. We need to change
the active storage configurations in the test because, otherwise, the
moment `ActiveStorage::Blob` is loaded, the `TenantDiskService` class is
also loaded, meaning the test will pass when using `is_a?`.

Note that, since this class isn't in the autoload paths anymore, we need
to add a `require` in the tests. We could add an initializer to require
it; we're not doing it in order to be consistent with what ActiveStorage
does: it only loads the service that's going to be used in the current
Rails environment. If somebody changed their production environment in
order to use (for example), S3, and we added an initializer to require
the TenantDiskService, we would still load the TenantDiskService even if
it isn't going to be used.

[1] https://guides.rubyonrails.org/v7.1/configuring.html#config-add-autoload-paths-to-load-path
[2] https://guides.rubyonrails.org/v7.1/upgrading_ruby_on_rails.html#autoloaded-paths-are-no-longer-in-$load-path
2024-04-17 15:18:41 +02:00

477 lines
16 KiB
Ruby

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