Files
fluxer/packages/config/src/__tests__/ConfigLoader.test.ts
T

1122 lines
42 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {generateKeyPairSync} from 'node:crypto';
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {getConfig, loadConfig, resetConfig} from '@fluxer/config/src/ConfigLoader';
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
const MINIMAL_ENV: Record<string, string> = {
FLUXER_ENV: 'test',
FLUXER_BASE_DOMAIN: 'localhost',
FLUXER_PUBLIC_SCHEME: 'http',
FLUXER_PUBLIC_PORT: '8088',
FLUXER_CASSANDRA_HOSTS: '127.0.0.1',
FLUXER_CASSANDRA_PORT: '9042',
FLUXER_CASSANDRA_KEYSPACE: 'fluxer_test',
FLUXER_CASSANDRA_LOCAL_DC: 'datacenter1',
FLUXER_CASSANDRA_USERNAME: 'test-user',
FLUXER_CASSANDRA_PASSWORD: 'test-password',
FLUXER_S3_ENDPOINT: 'http://127.0.0.1:3900',
FLUXER_S3_ACCESS_KEY_ID: 'test-key',
FLUXER_S3_SECRET_ACCESS_KEY: 'test-secret',
FLUXER_MEDIA_PROXY_SECRET_KEY: 'test-media-secret',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=',
FLUXER_ADMIN_SECRET_KEY_BASE: 'test-admin-secret',
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: 'test-admin-oauth-secret',
FLUXER_GATEWAY_RPC_AUTH_TOKEN: 'test-gateway-token',
FLUXER_SUDO_MODE_SECRET: 'test-sudo-secret',
FLUXER_CONNECTION_INITIATION_SECRET: 'test-connection-secret',
FLUXER_PROFILE_PSEUDONYM_SECRET: 'test-profile-pseudonym-secret',
FLUXER_VAPID_PUBLIC_KEY: 'BB76bTFIuoqmxJtTfZX0yGTn1f_qu9H03B_nkj8OyExJFkN7Y-HBZZzShnHZoEhXKc5ZRy3jFu7OkBbnaQG-4aw',
FLUXER_VAPID_PRIVATE_KEY: 'Xgi-3P8J-I3Q6U1HlCcXMuc_tKLGAM9nIfznX3Hz68o',
};
function generateVapidPair(): {publicKey: string; privateKey: string} {
const {privateKey} = generateKeyPairSync('ec', {namedCurve: 'prime256v1'});
const jwk = privateKey.export({format: 'jwk'});
const x = Buffer.from(jwk.x ?? '', 'base64url');
const y = Buffer.from(jwk.y ?? '', 'base64url');
const d = Buffer.from(jwk.d ?? '', 'base64url');
return {
publicKey: Buffer.concat([Buffer.from([0x04]), x, y]).toString('base64url'),
privateKey: d.toString('base64url'),
};
}
function stubMinimalEnv(overrides: Record<string, string> = {}): void {
for (const [key, value] of Object.entries({...MINIMAL_ENV, ...overrides})) {
vi.stubEnv(key, value);
}
}
function clearFluxerEnv(): void {
for (const key of Object.keys(process.env)) {
if (key.startsWith('FLUXER_')) {
vi.stubEnv(key, undefined);
}
}
}
describe('ConfigLoader', () => {
beforeEach(() => {
resetConfig();
clearFluxerEnv();
});
afterEach(() => {
resetConfig();
vi.unstubAllEnvs();
});
test('loadConfig builds and caches config from FLUXER environment variables', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.env).toBe('test');
expect(config.domain.base_domain).toBe('localhost');
expect(config.database.backend).toBe('postgres');
expect(config.database.postgres.database).toBe('fluxer');
expect(config.database.cassandra.hosts).toEqual(['127.0.0.1']);
vi.stubEnv('FLUXER_BASE_DOMAIN', 'changed.example');
expect((await loadConfig()).domain.base_domain).toBe('localhost');
});
test('loadConfig reads secrets from NAME_FILE', async () => {
const dir = mkdtempSync(join(tmpdir(), 'fluxer-config-file-'));
try {
const path = join(dir, 'postgres_password');
writeFileSync(path, 'from-secret-file\n');
stubMinimalEnv({FLUXER_POSTGRES_PASSWORD: '', FLUXER_POSTGRES_PASSWORD_FILE: path});
const config = await loadConfig();
expect(config.database.postgres.password).toBe('from-secret-file');
} finally {
rmSync(dir, {recursive: true, force: true});
}
});
test('loadConfig rejects NAME and NAME_FILE together', async () => {
stubMinimalEnv({FLUXER_SUDO_MODE_SECRET_FILE: '/run/secrets/sudo'});
await expect(loadConfig()).rejects.toThrow('FLUXER_SUDO_MODE_SECRET and FLUXER_SUDO_MODE_SECRET_FILE are both set');
});
test('getConfig throws when config is not loaded', () => {
expect(() => getConfig()).toThrow('Config not loaded');
});
test('resetConfig clears the cache', async () => {
stubMinimalEnv();
await loadConfig();
expect(() => getConfig()).not.toThrow();
resetConfig();
expect(() => getConfig()).toThrow('Config not loaded');
});
test('derives endpoints from domain config', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.endpoints.api).toBe('http://localhost:8088/api');
expect(config.endpoints.gateway).toBe('ws://localhost:8088/gateway');
});
test('endpoint overrides take precedence over derived endpoints', async () => {
stubMinimalEnv({
FLUXER_API_ENDPOINT: 'https://custom-api.example.com',
FLUXER_API_CLIENT_ENDPOINT: 'https://custom-api-client.example.com',
FLUXER_GATEWAY_ENDPOINT: 'wss://custom-gw.example.com',
});
const config = await loadConfig();
expect(config.endpoints.api).toBe('https://custom-api.example.com');
expect(config.endpoints.api_client).toBe('https://custom-api-client.example.com');
expect(config.endpoints.gateway).toBe('wss://custom-gw.example.com');
expect(config.endpoints.app).toBe('http://localhost:8088');
});
test('inserts the public port into portless endpoints on a non-standard port', async () => {
stubMinimalEnv({
FLUXER_MARKETING_ENDPOINT: 'http://localhost',
FLUXER_MEDIA_ENDPOINT: 'http://localhost/media',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'http://localhost/media',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'http://localhost',
});
const config = await loadConfig();
expect(config.endpoints.marketing).toBe('http://localhost:8088');
expect(config.endpoints.media).toBe('http://localhost:8088/media');
expect(config.services.media_proxy.upload_relay.endpoint).toBe('http://localhost:8088/media');
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['http://localhost:8088']);
});
test('leaves a default https install untouched', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_MARKETING_ENDPOINT: 'https://chat.example',
FLUXER_MEDIA_ENDPOINT: 'https://chat.example/media',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'https://chat.example/media',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://chat.example',
});
const config = await loadConfig();
expect(config.endpoints.marketing).toBe('https://chat.example');
expect(config.endpoints.media).toBe('https://chat.example/media');
expect(config.endpoints.api).toBe('https://chat.example/api');
expect(config.endpoints.gateway).toBe('wss://chat.example/gateway');
expect(config.services.media_proxy.upload_relay.endpoint).toBe('https://chat.example/media');
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://chat.example']);
});
test('leaves a default http install untouched', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example',
FLUXER_PUBLIC_SCHEME: 'http',
FLUXER_PUBLIC_PORT: '80',
FLUXER_MARKETING_ENDPOINT: 'http://chat.example',
FLUXER_MEDIA_ENDPOINT: 'http://chat.example/media',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'http://chat.example/media',
});
const config = await loadConfig();
expect(config.endpoints.marketing).toBe('http://chat.example');
expect(config.endpoints.media).toBe('http://chat.example/media');
expect(config.endpoints.gateway).toBe('ws://chat.example/gateway');
expect(config.services.media_proxy.upload_relay.endpoint).toBe('http://chat.example/media');
});
test('leaves foreign hosts and already ported endpoints untouched', async () => {
stubMinimalEnv({
FLUXER_STATIC_CDN_ENDPOINT: 'https://cdn.example.net',
FLUXER_MEDIA_ENDPOINT: 'https://media.example.net/media',
FLUXER_MARKETING_ENDPOINT: 'http://localhost:9999',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'http://localhost:8088/media',
});
const config = await loadConfig();
expect(config.endpoints.static_cdn).toBe('https://cdn.example.net');
expect(config.endpoints.media).toBe('https://media.example.net/media');
expect(config.endpoints.marketing).toBe('http://localhost:9999');
expect(config.services.media_proxy.upload_relay.endpoint).toBe('http://localhost:8088/media');
});
test('normalizes each passkey origin independently', async () => {
stubMinimalEnv({
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS:
'http://localhost,http://localhost:3000,https://desktop.example.net,android:apk-key-hash:keSY4bimyLqZQV7bKXgpa2xYuqXi0qZJzsYtp6gpx7w',
});
const config = await loadConfig();
expect(config.auth.passkeys.additional_allowed_origins).toEqual([
'http://localhost:8088',
'http://localhost:3000',
'https://desktop.example.net',
'android:apk-key-hash:keSY4bimyLqZQV7bKXgpa2xYuqXi0qZJzsYtp6gpx7w',
]);
});
test('includes the app origin alongside the default passkey origins', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.auth.passkeys.additional_allowed_origins).toEqual([
'https://fluxer.app',
'https://web.fluxer.app',
'https://web.canary.fluxer.app',
'https://fluxer.com',
'https://canary.fluxer.com',
'android:apk-key-hash:keSY4bimyLqZQV7bKXgpa2xYuqXi0qZJzsYtp6gpx7w',
'http://localhost:8088',
]);
});
test('defaults to no app origin aliases', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.services.api.app_origin_aliases).toEqual([]);
});
test('normalizes and deduplicates app origin aliases', async () => {
stubMinimalEnv({
FLUXER_APP_ORIGIN_ALIASES:
'https://Web.Fluxer.App/, https://fluxer.com,https://fluxer.com:443,http://localhost:3000',
});
const config = await loadConfig();
expect(config.services.api.app_origin_aliases).toEqual([
'https://web.fluxer.app',
'https://fluxer.com',
'http://localhost:3000',
]);
});
test.each(['fluxer.com', 'https://fluxer.com/app', 'ftp://fluxer.com', 'https://[email protected]'])(
'rejects the app origin alias %s',
async (alias) => {
stubMinimalEnv({FLUXER_APP_ORIGIN_ALIASES: alias});
await expect(loadConfig()).rejects.toThrow('FLUXER_APP_ORIGIN_ALIASES entry 1 must be an HTTP(S) origin');
},
);
test('an empty client API endpoint override falls back to the derived endpoint', async () => {
stubMinimalEnv({FLUXER_API_CLIENT_ENDPOINT: ''});
expect((await loadConfig()).endpoints.api_client).toBe('http://localhost:8088/api');
});
test('defaults the passkey relying party to the deployment domain', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
});
const config = await loadConfig();
expect(config.auth.passkeys.rp_id).toBe('chat.example.com');
});
test('a blank origin list keeps the built-in origins', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: ' ',
});
const config = await loadConfig();
expect(config.auth.passkeys.additional_allowed_origins).toContain('https://web.fluxer.app');
expect(config.auth.passkeys.additional_allowed_origins).toContain('https://chat.example.com');
});
test('blank values fall back to the code defaults', async () => {
stubMinimalEnv({
FLUXER_API_PORT: '',
FLUXER_EMAIL_FROM_NAME: '',
FLUXER_KV_URL: ' ',
FLUXER_S3_FORCE_PATH_STYLE: '',
FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: '',
});
const config = await loadConfig();
expect(config.services.api.port).toBe(8080);
expect(config.integrations.email.from_name).toBe('Fluxer');
expect(config.internal.kv).toBe('redis://localhost:6379/0');
expect(config.s3?.force_path_style).toBe(false);
expect(config.services.api.storage_change_feed?.skip_buckets).toBeUndefined();
});
test('reads the email reply-to address', async () => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: '[email protected]'});
const config = await loadConfig();
expect(config.integrations.email.reply_to_email).toBe('[email protected]');
});
test('leaves the email reply-to address empty when unset or blank', async () => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: ' '});
const config = await loadConfig();
expect(config.integrations.email.reply_to_email).toBe('');
});
test.each(['support', 'Support <[email protected]>', '[email protected],[email protected]', ' [email protected]'])(
'rejects %j as the email reply-to address',
async (value) => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: value});
await expect(loadConfig()).rejects.toThrow('FLUXER_EMAIL_REPLY_TO_EMAIL must be a single email address');
},
);
test('keeps explicit passkey relying party values', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PASSKEY_RP_ID: 'example.com',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://example.com,https://app.example.com',
});
const config = await loadConfig();
expect(config.auth.passkeys.rp_id).toBe('example.com');
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://example.com', 'https://app.example.com']);
});
test('parses typed named environment variables', async () => {
stubMinimalEnv({
FLUXER_API_PORT: '9090',
FLUXER_CASSANDRA_HOSTS: 'db1,db2',
FLUXER_POSTGRES_HOST: 'pg1',
FLUXER_POSTGRES_PORT: '5544',
FLUXER_POSTGRES_MAX_CONNECTIONS: '7',
FLUXER_POSTGRES_SSL_CA: '-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----',
FLUXER_POSTGRES_PREPARED_STATEMENTS: 'false',
FLUXER_API_WORKER_MODE: 'single_task',
FLUXER_API_WORKER_TASK: 'processStripeWebhook',
FLUXER_LIVEKIT_ENABLED: 'true',
FLUXER_LIVEKIT_DEFAULT_REGION:
'{"id":"local","name":"Local","emoji":"LC","latitude":59.3293,"longitude":18.0686}',
});
const config = await loadConfig();
expect(config.services.api.port).toBe(9090);
expect(config.database.cassandra.hosts).toEqual(['db1', 'db2']);
expect(config.database.postgres.host).toBe('pg1');
expect(config.database.postgres.port).toBe(5544);
expect(config.database.postgres.max_connections).toBe(7);
expect(config.database.postgres.ssl_ca).toBe('-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----');
expect(config.database.postgres.prepared_statements).toBe(false);
expect(config.services.api.worker?.mode).toBe('single_task');
expect(config.services.api.worker?.task).toBe('processStripeWebhook');
expect(config.integrations.voice.default_region?.id).toBe('local');
});
test('ignores FLUXER_GATEWAY_PUSH_ENABLED, which only the gateway reads', async () => {
stubMinimalEnv({FLUXER_GATEWAY_PUSH_ENABLED: 'false'});
const config = await loadConfig();
expect(config.services.gateway).not.toHaveProperty('push_enabled');
});
test('accepts the crosspost worker lane', async () => {
stubMinimalEnv({
FLUXER_API_WORKER_MODE: 'single_lane',
FLUXER_API_WORKER_LANE: 'crosspost',
FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES: '{"crosspost":4}',
});
const config = await loadConfig();
expect(config.services.api.worker?.lane).toBe('crosspost');
expect(config.services.api.worker?.lane_concurrency_overrides?.crosspost).toBe(4);
});
test('rejects an unknown worker lane', async () => {
stubMinimalEnv({FLUXER_API_WORKER_MODE: 'single_lane', FLUXER_API_WORKER_LANE: 'publishing'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_WORKER_LANE');
});
test('rejects single task worker mode without task env', async () => {
stubMinimalEnv({FLUXER_API_WORKER_MODE: 'single_task'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_WORKER_TASK');
});
test('leaves the storage change feed disabled by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.services.api.storage_change_feed).toEqual({enabled: false, stream: 'STORAGE_CHANGES'});
});
test('maps the storage change feed environment variables', async () => {
stubMinimalEnv({
FLUXER_API_STORAGE_CHANGE_FEED_ENABLED: 'true',
FLUXER_API_STORAGE_CHANGE_FEED_STREAM: 'BACKUP_CHANGES',
FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: 'fluxer-uploads, fluxer-harvests',
});
const config = await loadConfig();
expect(config.services.api.storage_change_feed).toEqual({
enabled: true,
stream: 'BACKUP_CHANGES',
skip_buckets: ['fluxer-uploads', 'fluxer-harvests'],
});
});
test('rejects a storage change feed stream name that JetStream cannot use', async () => {
stubMinimalEnv({
FLUXER_API_STORAGE_CHANGE_FEED_ENABLED: 'true',
FLUXER_API_STORAGE_CHANGE_FEED_STREAM: 'storage.changes',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_STORAGE_CHANGE_FEED_STREAM');
});
test('rejects invalid Postgres typed environment values', async () => {
stubMinimalEnv({FLUXER_POSTGRES_PORT: 'abc'});
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_PORT');
});
test('rejects a non-integer port', async () => {
stubMinimalEnv({FLUXER_API_PORT: '80a'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_PORT must be an integer, got "80a"');
});
test('rejects malformed JSON for a JSON-shaped variable', async () => {
stubMinimalEnv({FLUXER_LIVEKIT_DEFAULT_REGION: '{bad'});
await expect(loadConfig()).rejects.toThrow('FLUXER_LIVEKIT_DEFAULT_REGION must be valid JSON');
});
test('keeps Postgres prepared statements on by default', async () => {
stubMinimalEnv();
expect((await loadConfig()).database.postgres.prepared_statements).toBe(true);
});
test('rejects a non-boolean Postgres prepared statements value', async () => {
stubMinimalEnv({FLUXER_POSTGRES_PREPARED_STATEMENTS: 'maybe'});
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_PREPARED_STATEMENTS');
});
test('keeps the api http timeouts at their defaults', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.services.api.headers_timeout_ms).toBe(30_000);
expect(config.services.api.request_timeout_ms).toBe(120_000);
});
test('reads the api http timeouts from the environment', async () => {
stubMinimalEnv({FLUXER_API_HEADERS_TIMEOUT_MS: '45000', FLUXER_API_REQUEST_TIMEOUT_MS: '600000'});
const config = await loadConfig();
expect(config.services.api.headers_timeout_ms).toBe(45_000);
expect(config.services.api.request_timeout_ms).toBe(600_000);
});
test('rejects a non-numeric api header timeout', async () => {
stubMinimalEnv({FLUXER_API_HEADERS_TIMEOUT_MS: 'soon'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_HEADERS_TIMEOUT_MS');
});
test('rejects a non-numeric api request timeout', async () => {
stubMinimalEnv({FLUXER_API_REQUEST_TIMEOUT_MS: 'soon'});
await expect(loadConfig()).rejects.toThrow('FLUXER_API_REQUEST_TIMEOUT_MS');
});
test('applies the KV mode from the environment', async () => {
stubMinimalEnv({FLUXER_KV_MODE: 'cluster'});
expect((await loadConfig()).internal.kv_mode).toBe('cluster');
});
test('rejects an unknown KV mode', async () => {
stubMinimalEnv({FLUXER_KV_MODE: 'sentinel'});
await expect(loadConfig()).rejects.toThrow('Invalid FLUXER_KV_MODE: sentinel');
});
test('reads the profile pseudonym secret and requires it in production', async () => {
stubMinimalEnv();
expect((await loadConfig()).auth.profile_pseudonym_secret).toBe('test-profile-pseudonym-secret');
resetConfig();
stubMinimalEnv({FLUXER_PROFILE_PSEUDONYM_SECRET: ''});
expect((await loadConfig()).auth.profile_pseudonym_secret).toBe('fluxer-dev-profile-pseudonym-secret');
resetConfig();
stubMinimalEnv({
FLUXER_ENV: 'production',
FLUXER_POSTGRES_HOST: 'postgres.internal',
FLUXER_POSTGRES_DATABASE: 'fluxer_prod',
FLUXER_POSTGRES_USERNAME: 'fluxer_app',
FLUXER_POSTGRES_PASSWORD: 'prod-postgres-secret',
FLUXER_POSTGRES_SSL: 'true',
FLUXER_PROFILE_PSEUDONYM_SECRET: '',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_PROFILE_PSEUDONYM_SECRET is required');
});
test('rejects unsafe production Postgres defaults', async () => {
stubMinimalEnv({FLUXER_ENV: 'production'});
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_HOST');
});
test('accepts explicitly configured production Postgres with TLS', async () => {
stubMinimalEnv({
FLUXER_ENV: 'production',
FLUXER_POSTGRES_HOST: 'postgres.internal',
FLUXER_POSTGRES_DATABASE: 'fluxer_prod',
FLUXER_POSTGRES_USERNAME: 'fluxer_app',
FLUXER_POSTGRES_PASSWORD: 'prod-postgres-secret',
FLUXER_POSTGRES_SSL: 'true',
});
const config = await loadConfig();
expect(config.database.postgres.host).toBe('postgres.internal');
expect(config.database.postgres.ssl).toBe(true);
});
test('allows self-hosted production Postgres without TLS', async () => {
stubMinimalEnv({
FLUXER_ENV: 'production',
FLUXER_SELF_HOSTED: 'true',
FLUXER_POSTGRES_HOST: 'postgres',
FLUXER_POSTGRES_DATABASE: 'fluxer',
FLUXER_POSTGRES_USERNAME: 'fluxer',
FLUXER_POSTGRES_PASSWORD: 'self-hosted-postgres-secret',
FLUXER_POSTGRES_SSL: 'false',
});
const config = await loadConfig();
expect(config.instance.self_hosted).toBe(true);
expect(config.database.postgres.ssl).toBe(false);
});
test('still requires TLS for non-self-hosted production Postgres', async () => {
stubMinimalEnv({
FLUXER_ENV: 'production',
FLUXER_POSTGRES_HOST: 'postgres.internal',
FLUXER_POSTGRES_DATABASE: 'fluxer_prod',
FLUXER_POSTGRES_USERNAME: 'fluxer_app',
FLUXER_POSTGRES_PASSWORD: 'prod-postgres-secret',
FLUXER_POSTGRES_SSL: 'false',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_POSTGRES_SSL must be true');
});
test('parses self-host branding, setup, and search engine environment variables', async () => {
stubMinimalEnv({
FLUXER_SEARCH_ENGINE: 'meilisearch',
FLUXER_SEARCH_URL: 'http://meilisearch:7700',
FLUXER_SEARCH_API_KEY: 'meili-key',
FLUXER_SELF_HOSTED: 'true',
FLUXER_APP_PRODUCT_NAME: 'Example Chat',
FLUXER_APP_ICON_URL: 'https://assets.example/icon.png',
FLUXER_APP_SYMBOL_URL: 'https://assets.example/symbol.png',
FLUXER_APP_LOGO_URL: 'https://assets.example/logo.png',
FLUXER_APP_WORDMARK_URL: 'https://assets.example/wordmark.png',
FLUXER_APP_FAVICON_URL: 'https://assets.example/favicon.png',
FLUXER_APP_THEME_COLOR: '#123456',
FLUXER_APP_STATUS_PAGE_URL: 'https://status.example',
FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: 'https://status.example/history',
FLUXER_INSTANCE_SETUP_CONFIGURED: 'true',
});
const config = await loadConfig();
expect(config.integrations.search.engine).toBe('meilisearch');
expect(config.integrations.search.url).toBe('http://meilisearch:7700');
expect(config.integrations.search.api_key).toBe('meili-key');
expect(config.instance.self_hosted).toBe(true);
expect(config.instance.branding).toEqual({
product_name: 'Example Chat',
icon_url: 'https://assets.example/icon.png',
symbol_url: 'https://assets.example/symbol.png',
logo_url: 'https://assets.example/logo.png',
wordmark_url: 'https://assets.example/wordmark.png',
favicon_url: 'https://assets.example/favicon.png',
theme_color: '#123456',
status_page_url: 'https://status.example',
status_page_incident_history_url: 'https://status.example/history',
});
expect(config.instance.setup.configured).toBe(true);
});
test('leaves the account identity unset by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.instance.account_identity).toBeNull();
});
test('treats an empty account identity as unset', async () => {
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: ' '});
const config = await loadConfig();
expect(config.instance.account_identity).toBeNull();
});
test.each([
['email', 'email'],
['username', 'username'],
[' Username ', 'username'],
])('parses FLUXER_ACCOUNT_IDENTITY=%j', async (raw, expected) => {
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: raw});
const config = await loadConfig();
expect(config.instance.account_identity).toBe(expected);
});
test.each([
['none', 'none'],
[' Random ', 'random'],
['', null],
])('parses FLUXER_TAG_STYLE=%j', async (raw, expected) => {
stubMinimalEnv({FLUXER_TAG_STYLE: raw});
const config = await loadConfig();
expect(config.instance.tag_style).toBe(expected);
});
test.each(['sequential', 'zero_first'])('rejects the tag style %j', async (raw) => {
stubMinimalEnv({FLUXER_TAG_STYLE: raw});
await expect(loadConfig()).rejects.toThrow('FLUXER_TAG_STYLE must be none or random');
});
test('rejects an unknown account identity', async () => {
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: 'phone'});
await expect(loadConfig()).rejects.toThrow('FLUXER_ACCOUNT_IDENTITY must be email or username');
});
test('leaves both stores off with no apps, packages or products by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.integrations.app_store).toEqual({enabled: false, apps: [], products: {}});
expect(config.integrations.google_play).toEqual({
enabled: false,
packages: [],
token_uri: 'https://oauth2.googleapis.com/token',
products: {},
});
expect(config.integrations.store_billing).toEqual({sandbox_user_ids: [], sandbox_entitles_all: false});
});
test('loads a valid store catalogue', async () => {
stubMinimalEnv({
FLUXER_APP_STORE_ENABLED: 'true',
FLUXER_APP_STORE_APPS: '[{"bundle_id":"com.fluxer","app_apple_id":1234567890}]',
FLUXER_APP_STORE_PRODUCTS: '{"com.fluxer.plutonium.monthly":"monthly","com.fluxer.gift.1month":"gift_1_month"}',
FLUXER_GOOGLE_PLAY_ENABLED: 'true',
FLUXER_GOOGLE_PLAY_PACKAGES: 'com.fluxer',
FLUXER_GOOGLE_PLAY_PRODUCTS: '{"plutonium:yearly":"yearly","gift_1_year":"gift_1_year"}',
FLUXER_STORE_BILLING_SANDBOX_USER_IDS: '1234567890123456',
});
const config = await loadConfig();
expect(config.integrations.app_store.apps).toEqual([{bundle_id: 'com.fluxer', app_apple_id: 1234567890}]);
expect(config.integrations.google_play.products).toEqual({
'plutonium:yearly': 'yearly',
gift_1_year: 'gift_1_year',
});
expect(config.integrations.store_billing.sandbox_user_ids).toEqual(['1234567890123456']);
});
test('rejects an App Store app without a numeric app_apple_id', async () => {
for (const apps of [
'[{"bundle_id":"com.fluxer"}]',
'[{"bundle_id":"com.fluxer","app_apple_id":"1234567890"}]',
'[{"bundle_id":"com.fluxer","app_apple_id":0}]',
]) {
resetConfig();
stubMinimalEnv({FLUXER_APP_STORE_APPS: apps});
await expect(loadConfig()).rejects.toThrow('FLUXER_APP_STORE_APPS entry 1 must have a numeric app_apple_id');
}
});
test('rejects an App Store app without a bundle id', async () => {
stubMinimalEnv({FLUXER_APP_STORE_APPS: '[{"app_apple_id":1234567890}]'});
await expect(loadConfig()).rejects.toThrow('FLUXER_APP_STORE_APPS entry 1 must have a bundle_id');
});
test('rejects an App Store product mapped to an unknown slot', async () => {
stubMinimalEnv({FLUXER_APP_STORE_PRODUCTS: '{"com.fluxer.visionary":"visionary"}'});
await expect(loadConfig()).rejects.toThrow(
'Invalid FLUXER_APP_STORE_PRODUCTS slot for com.fluxer.visionary: visionary',
);
});
test('rejects a Google Play product mapped to an unknown slot', async () => {
stubMinimalEnv({FLUXER_GOOGLE_PLAY_PRODUCTS: '{"plutonium:weekly":"weekly"}'});
await expect(loadConfig()).rejects.toThrow('Invalid FLUXER_GOOGLE_PLAY_PRODUCTS slot for plutonium:weekly: weekly');
});
test('rejects a Google Play product key that does not fit its slot kind', async () => {
stubMinimalEnv({FLUXER_GOOGLE_PLAY_PRODUCTS: '{"plutonium":"monthly"}'});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_GOOGLE_PLAY_PRODUCTS key plutonium does not match its monthly slot',
);
resetConfig();
stubMinimalEnv({FLUXER_GOOGLE_PLAY_PRODUCTS: '{"gift:one":"gift_1_month"}'});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_GOOGLE_PLAY_PRODUCTS key gift:one does not match its gift_1_month slot',
);
});
test('rejects a sandbox user id that is not a snowflake', async () => {
stubMinimalEnv({FLUXER_STORE_BILLING_SANDBOX_USER_IDS: '123,abc'});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_STORE_BILLING_SANDBOX_USER_IDS must be a comma separated list of user ids',
);
});
test('defaults the cache purge adapter to none', async () => {
stubMinimalEnv();
expect((await loadConfig()).integrations.cache_purge).toEqual({
adapter: 'none',
http: {endpoint: '', token: '', timeout_ms: 10_000},
});
});
test('reads the http cache purge settings from the environment', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TOKEN: 'purge-token',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: '5000',
});
expect((await loadConfig()).integrations.cache_purge).toEqual({
adapter: 'http',
http: {endpoint: 'https://purge.internal/purge', token: 'purge-token', timeout_ms: 5000},
});
});
test('rejects an unknown cache purge adapter', async () => {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'varnish'});
await expect(loadConfig()).rejects.toThrow('Invalid FLUXER_CACHE_PURGE_ADAPTER: varnish');
});
test('rejects the http cache purge adapter without an endpoint', async () => {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'http'});
await expect(loadConfig()).rejects.toThrow('FLUXER_CACHE_PURGE_HTTP_ENDPOINT is required');
});
test('rejects the http cache purge adapter without a token', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
});
await expect(loadConfig()).rejects.toThrow('FLUXER_CACHE_PURGE_HTTP_TOKEN is required');
});
test('rejects a cache purge endpoint that is not an absolute http URL', async () => {
for (const endpoint of ['/purge', 'purge.internal/purge', 'ftp://purge.internal/purge']) {
stubMinimalEnv({FLUXER_CACHE_PURGE_ADAPTER: 'http', FLUXER_CACHE_PURGE_HTTP_ENDPOINT: endpoint});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials',
);
}
});
test('rejects a cache purge endpoint that contains credentials', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purger:[email protected]/purge',
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_ENDPOINT must be an absolute http or https URL without credentials',
);
});
test('rejects a cache purge timeout outside 1000 to 10000', async () => {
for (const timeout of ['999', '10001']) {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TOKEN: 'purge-token',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: timeout,
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS must be an integer between 1000 and 10000',
);
}
});
test('rejects a cache purge token with spaces or control characters without echoing it', async () => {
for (const token of ['secret\nvalue', 'secret\rvalue', 'secret value', 'secret-value\n']) {
stubMinimalEnv({
FLUXER_CACHE_PURGE_ADAPTER: 'http',
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'https://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TOKEN: token,
});
await expect(loadConfig()).rejects.toThrow(
/^FLUXER_CACHE_PURGE_HTTP_TOKEN must contain only visible ASCII characters$/,
);
}
});
test('leaves the cache purge settings unvalidated when the adapter is none', async () => {
stubMinimalEnv({
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: 'ftp://purge.internal/purge',
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: '1',
});
expect((await loadConfig()).integrations.cache_purge.adapter).toBe('none');
});
test('leaves the optional outbound lookups unset by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.integrations.breached_password_check.enabled).toBeUndefined();
});
test('reads the optional outbound lookup switches from the environment', async () => {
stubMinimalEnv({
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: 'false',
});
const config = await loadConfig();
expect(config.integrations.breached_password_check.enabled).toBe(false);
});
test('leaves Bluesky login off with no legal URLs by default', async () => {
stubMinimalEnv();
const config = await loadConfig();
expect(config.auth.bluesky.enabled).toBe(false);
expect(config.auth.bluesky.tos_uri).toBe('');
expect(config.auth.bluesky.policy_uri).toBe('');
expect(config.auth.bluesky.keys).toEqual([]);
});
test('applies explicit Bluesky legal URLs from the environment', async () => {
stubMinimalEnv({
FLUXER_AUTH_BLUESKY_ENABLED: 'true',
FLUXER_AUTH_BLUESKY_TOS_URI: 'https://chat.example.com/terms',
FLUXER_AUTH_BLUESKY_POLICY_URI: 'https://chat.example.com/privacy',
});
const config = await loadConfig();
expect(config.auth.bluesky.enabled).toBe(true);
expect(config.auth.bluesky.tos_uri).toBe('https://chat.example.com/terms');
expect(config.auth.bluesky.policy_uri).toBe('https://chat.example.com/privacy');
});
test('reads the upload relay secret through the override table', async () => {
const secret = Buffer.alloc(32, 9).toString('base64');
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: secret});
const config = await loadConfig();
expect(config.services.media_proxy.upload_relay.secret_base64).toBe(secret);
});
test('defaults the upload relay body limit to the media proxy ceiling', async () => {
stubMinimalEnv();
expect((await loadConfig()).services.media_proxy.upload_relay.max_body_bytes).toBe(524_288_000);
});
test('rejects a missing upload relay secret', async () => {
stubMinimalEnv();
vi.stubEnv('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64', '');
await expect(loadConfig()).rejects.toThrow('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 is required');
});
test('rejects a non-base64 upload relay secret', async () => {
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: 'not base64!'});
await expect(loadConfig()).rejects.toThrow('FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must be base64');
});
test('rejects an upload relay secret shorter than 32 bytes', async () => {
stubMinimalEnv({FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: Buffer.alloc(16, 7).toString('base64')});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 must decode to at least 32 bytes',
);
});
test('rejects a VAPID public key that is not a 65-byte uncompressed point', async () => {
const {privateKey} = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: Buffer.alloc(64, 4).toString('base64url'),
FLUXER_VAPID_PRIVATE_KEY: privateKey,
});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_VAPID_PUBLIC_KEY must be the base64url 65-byte uncompressed P-256 point',
);
});
test('rejects a VAPID private key that is not a 32-byte scalar', async () => {
const {publicKey} = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: publicKey,
FLUXER_VAPID_PRIVATE_KEY: Buffer.alloc(31, 9).toString('base64url'),
});
await expect(loadConfig()).rejects.toThrow('FLUXER_VAPID_PRIVATE_KEY must be the base64url 32-byte P-256 scalar');
});
test('rejects a well formed VAPID scalar that does not derive the public point', async () => {
const {publicKey} = generateVapidPair();
const other = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: publicKey,
FLUXER_VAPID_PRIVATE_KEY: other.privateKey,
});
await expect(loadConfig()).rejects.toThrow('FLUXER_VAPID_PRIVATE_KEY does not match FLUXER_VAPID_PUBLIC_KEY');
});
test('accepts a generated VAPID pair', async () => {
const pair = generateVapidPair();
stubMinimalEnv({
FLUXER_VAPID_PUBLIC_KEY: pair.publicKey,
FLUXER_VAPID_PRIVATE_KEY: pair.privateKey,
});
const config = await loadConfig();
expect(config.auth.vapid.public_key).toBe(pair.publicKey);
expect(config.auth.vapid.private_key).toBe(pair.privateKey);
});
test('requires a complete environment', async () => {
vi.stubEnv('FLUXER_ENV', 'test');
await expect(loadConfig()).rejects.toThrow();
});
test('inserts the public port into the LiveKit url', async () => {
stubMinimalEnv({FLUXER_LIVEKIT_URL: 'http://localhost/livekit'});
const config = await loadConfig();
expect(config.integrations.voice.url).toBe('http://localhost:8088/livekit');
});
test('inserts the public port into every other public url the config contains', async () => {
stubMinimalEnv({
FLUXER_S3_PUBLIC_ENDPOINT: 'http://localhost/s3',
FLUXER_EMAIL_APP_BASE_URL: 'http://localhost',
FLUXER_AUTH_BLUESKY_CLIENT_URI: 'http://localhost',
FLUXER_AUTH_BLUESKY_TOS_URI: 'http://localhost/terms',
FLUXER_APP_ICON_URL: 'http://localhost/icon.png',
FLUXER_LIVEKIT_INTERNAL_URL: 'http://livekit:7880',
});
const config = await loadConfig();
expect(config.s3?.presigned_url_base).toBe('http://localhost:8088/s3');
expect(config.integrations.email.app_base_url).toBe('http://localhost:8088');
expect(config.auth.bluesky.client_uri).toBe('http://localhost:8088');
expect(config.auth.bluesky.tos_uri).toBe('http://localhost:8088/terms');
expect(config.instance.branding.icon_url).toBe('http://localhost:8088/icon.png');
expect(config.integrations.voice.internal_url).toBe('http://livekit:7880');
});
test('rejects a public port outside the valid range', async () => {
stubMinimalEnv({FLUXER_PUBLIC_PORT: '70000'});
await expect(loadConfig()).rejects.toThrow('FLUXER_PUBLIC_PORT must be an integer between 1 and 65535');
});
});
describe('FLUXER_PUBLIC_ORIGIN', () => {
beforeEach(() => {
resetConfig();
clearFluxerEnv();
});
afterEach(() => {
resetConfig();
vi.unstubAllEnvs();
});
test('a ported origin ports every derived endpoint and every compose override', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
FLUXER_MARKETING_ENDPOINT: 'https://chat.example.com:29080',
FLUXER_MEDIA_ENDPOINT: 'https://chat.example.com:29080/media',
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: 'https://chat.example.com:29080/media',
FLUXER_LIVEKIT_URL: 'https://chat.example.com:29080/livekit',
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: 'https://chat.example.com:29080',
});
const config = await loadConfig();
expect(config.domain.public_port).toBe(29080);
expect(config.domain.public_origin).toBe('https://chat.example.com:29080');
expect(config.endpoints.api_client).toBe('https://chat.example.com:29080/api');
expect(config.endpoints.app).toBe('https://chat.example.com:29080');
expect(config.endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
expect(config.endpoints.admin).toBe('https://chat.example.com:29080/admin');
expect(config.endpoints.marketing).toBe('https://chat.example.com:29080');
expect(config.endpoints.media).toBe('https://chat.example.com:29080/media');
expect(config.services.media_proxy.upload_relay.endpoint).toBe('https://chat.example.com:29080/media');
expect(config.integrations.voice.url).toBe('https://chat.example.com:29080/livekit');
expect(config.auth.passkeys.additional_allowed_origins).toEqual(['https://chat.example.com:29080']);
});
test('the origin port wins over a FLUXER_PUBLIC_PORT that disagrees', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
});
const config = await loadConfig();
expect(config.domain.public_port).toBe(29080);
expect(config.endpoints.app).toBe('https://chat.example.com:29080');
expect(config.endpoints.api_client).toBe('https://chat.example.com:29080/api');
expect(config.endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
expect(config.endpoints.admin).toBe('https://chat.example.com:29080/admin');
});
test('accepts an origin whose port matches FLUXER_PUBLIC_PORT', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '29080',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
});
expect((await loadConfig()).endpoints.gateway).toBe('wss://chat.example.com:29080/gateway');
});
test('normalizes an origin written with an explicit default port', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '443',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:443',
FLUXER_ADMIN_ENDPOINT: 'https://chat.example.com/admin',
});
const config = await loadConfig();
expect(config.domain.public_port).toBe(443);
expect(config.domain.public_origin).toBe('https://chat.example.com');
expect(config.endpoints.admin).toBe('https://chat.example.com/admin');
expect(config.endpoints.app).toBe('https://chat.example.com');
expect(config.endpoints.gateway).toBe('wss://chat.example.com/gateway');
});
test('takes the scheme and the domain from the origin when neither is set', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: '',
FLUXER_PUBLIC_SCHEME: '',
FLUXER_PUBLIC_PORT: '',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com:29080',
});
const config = await loadConfig();
expect(config.domain.base_domain).toBe('chat.example.com');
expect(config.domain.public_scheme).toBe('https');
expect(config.domain.public_port).toBe(29080);
});
test('the origin scheme wins over a FLUXER_PUBLIC_SCHEME that disagrees', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'http',
FLUXER_PUBLIC_PORT: '',
FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com',
});
const config = await loadConfig();
expect(config.domain.public_scheme).toBe('https');
expect(config.endpoints.app).toBe('https://chat.example.com');
expect(config.endpoints.gateway).toBe('wss://chat.example.com/gateway');
});
test('the origin host wins over a FLUXER_BASE_DOMAIN that disagrees', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
FLUXER_PUBLIC_SCHEME: 'https',
FLUXER_PUBLIC_PORT: '',
FLUXER_PUBLIC_ORIGIN: 'https://other.example.com',
});
const config = await loadConfig();
expect(config.domain.base_domain).toBe('other.example.com');
expect(config.endpoints.app).toBe('https://other.example.com');
});
test('refuses to boot on an origin that is not a bare origin', async () => {
stubMinimalEnv({FLUXER_PUBLIC_ORIGIN: 'https://chat.example.com/app'});
await expect(loadConfig()).rejects.toThrow(
'FLUXER_PUBLIC_ORIGIN must be a scheme, host and optional port such as https://chat.example.com:8443',
);
});
test('leaves the derived origin canonical when nothing is set', async () => {
stubMinimalEnv();
expect((await loadConfig()).domain.public_origin).toBe('http://localhost:8088');
});
});