# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # frozen_string_literal: true require "spec_helper" RSpec.describe PotatoMesh::Config do describe ".data_directory" do it "uses the configured XDG data home when provided" do Dir.mktmpdir do |dir| data_home = File.join(dir, "xdg-data") within_env("XDG_DATA_HOME" => data_home) do expect(described_class.data_directory).to eq(File.join(data_home, "potato-mesh")) end end end it "falls back to the user home directory" do within_env("XDG_DATA_HOME" => nil) do allow(Dir).to receive(:home).and_return("/home/spec") expect(described_class.data_directory).to eq("/home/spec/.local/share/potato-mesh") end ensure allow(Dir).to receive(:home).and_call_original end it "falls back to the web root when the home directory is unavailable" do within_env("XDG_DATA_HOME" => nil) do allow(Dir).to receive(:home).and_raise(ArgumentError) expected = File.join(described_class.web_root, ".local", "share", "potato-mesh") expect(described_class.data_directory).to eq(expected) end ensure allow(Dir).to receive(:home).and_call_original end it "falls back to the web root when the home directory is nil" do within_env("XDG_DATA_HOME" => nil) do allow(Dir).to receive(:home).and_return(nil) expected = File.join(described_class.web_root, ".local", "share", "potato-mesh") expect(described_class.data_directory).to eq(expected) end ensure allow(Dir).to receive(:home).and_call_original end end describe ".config_directory" do it "uses the configured XDG config home when provided" do Dir.mktmpdir do |dir| config_home = File.join(dir, "xdg-config") within_env("XDG_CONFIG_HOME" => config_home) do expect(described_class.config_directory).to eq(File.join(config_home, "potato-mesh")) end end end it "falls back to the web root when the home directory is empty" do within_env("XDG_CONFIG_HOME" => nil) do allow(Dir).to receive(:home).and_return("") expected = File.join(described_class.web_root, ".config", "potato-mesh") expect(described_class.config_directory).to eq(expected) end ensure allow(Dir).to receive(:home).and_call_original end end describe ".legacy_config_directory" do it "returns the repository managed configuration directory" do expect(described_class.legacy_config_directory).to eq( File.join(described_class.web_root, ".config"), ) end end describe ".legacy_keyfile_path" do it "returns the legacy keyfile location" do expect(described_class.legacy_keyfile_path).to eq( File.join(described_class.web_root, ".config", "keyfile"), ) end it "prefers repository config keyfiles when present" do Dir.mktmpdir do |dir| web_root = File.join(dir, "web") legacy_key = File.join(web_root, "config", "potato-mesh", "keyfile") FileUtils.mkdir_p(File.dirname(legacy_key)) File.write(legacy_key, "legacy") allow(described_class).to receive(:web_root).and_return(web_root) expect(described_class.legacy_keyfile_path).to eq(legacy_key) end ensure allow(described_class).to receive(:web_root).and_call_original end end describe ".legacy_db_path" do it "returns the bundled database location" do expect(described_class.legacy_db_path).to eq( File.expand_path("../data/mesh.db", described_class.web_root), ) end end describe ".private_mode_enabled?" do it "returns false when PRIVATE is unset" do within_env("PRIVATE" => nil) do expect(described_class.private_mode_enabled?).to be(false) end end it "returns false when PRIVATE=0" do within_env("PRIVATE" => "0") do expect(described_class.private_mode_enabled?).to be(false) end end it "returns true when PRIVATE=1" do within_env("PRIVATE" => "1") do expect(described_class.private_mode_enabled?).to be(true) end end it "ignores surrounding whitespace" do within_env("PRIVATE" => " 1 ") do expect(described_class.private_mode_enabled?).to be(true) end end end describe ".federation_enabled?" do it "returns true when FEDERATION is unset" do within_env("FEDERATION" => nil, "PRIVATE" => "0") do expect(described_class.federation_enabled?).to be(true) end end it "returns false when FEDERATION=0" do within_env("FEDERATION" => "0", "PRIVATE" => "0") do expect(described_class.federation_enabled?).to be(false) end end it "returns false when PRIVATE=1" do within_env("FEDERATION" => "1", "PRIVATE" => "1") do expect(described_class.federation_enabled?).to be(false) end end it "ignores surrounding whitespace" do within_env("FEDERATION" => " 0 ", "PRIVATE" => "0") do expect(described_class.federation_enabled?).to be(false) end end end describe ".legacy_well_known_candidates" do it "includes repository config directories" do Dir.mktmpdir do |dir| web_root = File.join(dir, "web") allow(described_class).to receive(:web_root).and_return(web_root) candidates = described_class.legacy_well_known_candidates expect(candidates).to include( File.join(web_root, "config", "potato-mesh", "well-known", "potato-mesh"), ) end ensure allow(described_class).to receive(:web_root).and_call_original end end describe ".federation_announcement_interval" do it "returns eight hours in seconds" do expect(described_class.federation_announcement_interval).to eq(8 * 60 * 60) end end describe ".remote_instance_http_timeout" do it "returns the baked-in connect timeout when unset" do within_env("REMOTE_INSTANCE_CONNECT_TIMEOUT" => nil) do expect(described_class.remote_instance_http_timeout).to eq( PotatoMesh::Config::DEFAULT_REMOTE_INSTANCE_CONNECT_TIMEOUT, ) end end it "accepts positive environment overrides" do within_env("REMOTE_INSTANCE_CONNECT_TIMEOUT" => "27") do expect(described_class.remote_instance_http_timeout).to eq(27) end end it "rejects non-positive overrides" do within_env("REMOTE_INSTANCE_CONNECT_TIMEOUT" => "0") do expect(described_class.remote_instance_http_timeout).to eq( PotatoMesh::Config::DEFAULT_REMOTE_INSTANCE_CONNECT_TIMEOUT, ) end end end describe ".remote_instance_read_timeout" do it "returns the baked-in read timeout when unset" do within_env("REMOTE_INSTANCE_READ_TIMEOUT" => nil) do expect(described_class.remote_instance_read_timeout).to eq( PotatoMesh::Config::DEFAULT_REMOTE_INSTANCE_READ_TIMEOUT, ) end end it "accepts positive overrides" do within_env("REMOTE_INSTANCE_READ_TIMEOUT" => "20") do expect(described_class.remote_instance_read_timeout).to eq(20) end end it "rejects non-positive overrides" do within_env("REMOTE_INSTANCE_READ_TIMEOUT" => "-5") do expect(described_class.remote_instance_read_timeout).to eq( PotatoMesh::Config::DEFAULT_REMOTE_INSTANCE_READ_TIMEOUT, ) end end end describe ".federation_max_instances_per_response" do it "returns the baked-in response limit when unset" do within_env("FEDERATION_MAX_INSTANCES_PER_RESPONSE" => nil) do expect(described_class.federation_max_instances_per_response).to eq( PotatoMesh::Config::DEFAULT_FEDERATION_MAX_INSTANCES_PER_RESPONSE, ) end end it "accepts positive overrides" do within_env("FEDERATION_MAX_INSTANCES_PER_RESPONSE" => "7") do expect(described_class.federation_max_instances_per_response).to eq(7) end end it "rejects non-positive overrides" do within_env("FEDERATION_MAX_INSTANCES_PER_RESPONSE" => "0") do expect(described_class.federation_max_instances_per_response).to eq( PotatoMesh::Config::DEFAULT_FEDERATION_MAX_INSTANCES_PER_RESPONSE, ) end end end describe ".federation_max_domains_per_crawl" do it "returns the baked-in crawl limit when unset" do within_env("FEDERATION_MAX_DOMAINS_PER_CRAWL" => nil) do expect(described_class.federation_max_domains_per_crawl).to eq( PotatoMesh::Config::DEFAULT_FEDERATION_MAX_DOMAINS_PER_CRAWL, ) end end it "accepts positive overrides" do within_env("FEDERATION_MAX_DOMAINS_PER_CRAWL" => "11") do expect(described_class.federation_max_domains_per_crawl).to eq(11) end end it "rejects invalid overrides" do within_env("FEDERATION_MAX_DOMAINS_PER_CRAWL" => "-5") do expect(described_class.federation_max_domains_per_crawl).to eq( PotatoMesh::Config::DEFAULT_FEDERATION_MAX_DOMAINS_PER_CRAWL, ) end end end describe ".db_path" do it "returns the default path inside the data directory" do expect(described_class.db_path).to eq(described_class.default_db_path) expect(described_class.db_path).to eq(File.join(described_class.data_directory, "mesh.db")) end end describe ".max_json_body_bytes" do it "returns the baked-in default size" do expect(described_class.max_json_body_bytes).to eq(described_class.default_max_json_body_bytes) end end describe ".refresh_interval_seconds" do it "returns the baked-in refresh cadence" do expect(described_class.refresh_interval_seconds).to eq(described_class.default_refresh_interval_seconds) end end describe ".prom_report_id_list" do it "returns an empty collection when no identifiers are configured" do expect(described_class.prom_report_id_list).to eq([]) end end describe ".channel" do it "returns the default channel when unset" do within_env("CHANNEL" => nil) do expect(described_class.channel).to eq(PotatoMesh::Config::DEFAULT_CHANNEL) end end it "trims whitespace from overrides" do within_env("CHANNEL" => " #Spec ") do expect(described_class.channel).to eq("#Spec") end end end describe ".frequency" do it "returns the default frequency when unset" do within_env("FREQUENCY" => nil) do expect(described_class.frequency).to eq(PotatoMesh::Config::DEFAULT_FREQUENCY) end end it "trims whitespace from overrides" do within_env("FREQUENCY" => " 915MHz ") do expect(described_class.frequency).to eq("915MHz") end end end describe ".map_center" do it "parses latitude and longitude from the environment" do within_env("MAP_CENTER" => "10.5, -20.25") do expect(described_class.map_center).to eq({ lat: 10.5, lon: -20.25 }) end end it "falls back to defaults when parsing fails" do within_env("MAP_CENTER" => "potato") do expect(described_class.map_center).to eq({ lat: PotatoMesh::Config::DEFAULT_MAP_CENTER_LAT, lon: PotatoMesh::Config::DEFAULT_MAP_CENTER_LON }) end end end describe ".max_distance_km" do it "returns the default distance when unset" do within_env("MAX_DISTANCE" => nil) do expect(described_class.max_distance_km).to eq(PotatoMesh::Config::DEFAULT_MAX_DISTANCE_KM) end end it "parses positive numeric overrides" do within_env("MAX_DISTANCE" => "105.5") do expect(described_class.max_distance_km).to eq(105.5) end end it "rejects invalid overrides" do within_env("MAX_DISTANCE" => "-1") do expect(described_class.max_distance_km).to eq(PotatoMesh::Config::DEFAULT_MAX_DISTANCE_KM) end end end describe ".contact_link" do it "returns the default contact when unset" do within_env("CONTACT_LINK" => nil) do expect(described_class.contact_link).to eq(PotatoMesh::Config::DEFAULT_CONTACT_LINK) end end it "trims whitespace from overrides" do within_env("CONTACT_LINK" => " https://example.org/chat ") do expect(described_class.contact_link).to eq("https://example.org/chat") end end end describe ".contact_link_url" do it "builds a matrix.to URL for aliases" do within_env("CONTACT_LINK" => "#spec:example.org") do expect(described_class.contact_link_url).to eq("https://matrix.to/#/#spec:example.org") end end it "passes through existing URLs" do within_env("CONTACT_LINK" => "https://example.org/chat") do expect(described_class.contact_link_url).to eq("https://example.org/chat") end end it "returns nil for unrecognised values" do within_env("CONTACT_LINK" => "Community Portal") do expect(described_class.contact_link_url).to be_nil end end end describe ".fetch_string" do it "trims whitespace and falls back when blank" do within_env("SITE_NAME" => " \t ") do expect(described_class.site_name).to eq("PotatoMesh Demo") end within_env("SITE_NAME" => " Spec Mesh ") do expect(described_class.site_name).to eq("Spec Mesh") end end end describe ".debug?" do it "reflects the DEBUG environment variable" do within_env("DEBUG" => "1") do expect(described_class.debug?).to be(true) end within_env("DEBUG" => nil) do expect(described_class.debug?).to be(false) end end end describe ".tile_filters" do it "returns a frozen mapping" do filters = described_class.tile_filters expect(filters).to match(light: String, dark: String) expect(filters).to be_frozen end end # Execute the provided block with temporary environment overrides. # # @param values [Hash{String=>String, nil}] key/value pairs to set in ENV. # @yield [] block executed while the overrides are active. # @return [void] def within_env(values) original = {} values.each do |key, value| original[key] = ENV.key?(key) ? ENV[key] : :__unset__ if value.nil? ENV.delete(key) else ENV[key] = value end end yield ensure original.each do |key, value| if value == :__unset__ ENV.delete(key) else ENV[key] = value end end end end