Files
fluxer/fluxer_api/src/api/instance/InstanceConfigRepository.test.ts
T

426 lines
17 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {setCassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution';
import type {PreparedQuery} from '@app/api/database/CassandraTypes';
import {
INSTANCE_CONFIG_REFRESH_CHANNEL,
InstanceConfigRepository,
type InstanceRegistrationConfig,
} from '@app/api/instance/InstanceConfigRepository';
import {InMemoryCassandraQueryExecutor} from '@app/api/test/InMemoryCassandraQueryExecutor';
import {MockKVProvider} from '@app/api/test/mocks/MockKVProvider';
import {
DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
type VoiceNoiseSuppressionConfig,
} from '@fluxer/schema/src/domains/admin/VoiceNoiseSuppressionSchemas';
import {
DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
type ExperimentDeliveryConfig,
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
import {afterEach, describe, expect, it, vi} from 'vitest';
const VOICE_NOISE_SUPPRESSION_CONFIG_KEY = 'voice_noise_suppression_config';
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
const APP_PUBLIC_CONFIG_KEY = 'app_public_config';
const INSTANCE_POLICY_CONFIG_KEY = 'instance_policy_config';
const INSTANCE_INTEGRATIONS_CONFIG_KEY = 'instance_integrations_config';
class CountingInMemoryCassandraQueryExecutor extends InMemoryCassandraQueryExecutor {
instanceConfigSelects = 0;
override async executeQuery<T = Record<string, unknown>>(query: PreparedQuery): Promise<Array<T>> {
if (query.kvMeta?.action === 'select' && query.kvMeta.table.name === 'instance_configuration') {
this.instanceConfigSelects++;
}
return super.executeQuery<T>(query);
}
}
describe('InstanceConfigRepository', () => {
const repositories: Array<InstanceConfigRepository> = [];
afterEach(() => {
for (const repository of repositories) {
repository.shutdown();
}
repositories.length = 0;
});
function createRepository(kvProvider: MockKVProvider): InstanceConfigRepository {
const repository = new InstanceConfigRepository(kvProvider);
repositories.push(repository);
return repository;
}
it('serves repeated config reads from the hydrated in-memory cache', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setRegistrationConfig({mode: 'closed'});
executor.instanceConfigSelects = 0;
expect(await repository.getRegistrationConfig()).toEqual({
mode: 'closed',
admin_registration_urls_enabled: true,
} satisfies InstanceRegistrationConfig);
expect(await repository.getRegistrationConfig()).toEqual({
mode: 'closed',
admin_registration_urls_enabled: true,
} satisfies InstanceRegistrationConfig);
expect(executor.instanceConfigSelects).toBe(0);
expect(kvProvider.getSubscription().subscribedChannels).toContain(INSTANCE_CONFIG_REFRESH_CHANNEL);
});
it('refreshes a hydrated cache after another repository publishes a config update', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const reader = createRepository(kvProvider);
const writer = createRepository(kvProvider);
expect(await reader.getRegistrationConfig()).toEqual({
mode: 'open',
admin_registration_urls_enabled: true,
} satisfies InstanceRegistrationConfig);
await writer.setRegistrationConfig({mode: 'approval'});
await vi.waitFor(async () => {
expect(await reader.getRegistrationConfig()).toEqual({
mode: 'approval',
admin_registration_urls_enabled: true,
} satisfies InstanceRegistrationConfig);
});
});
it('reuses the memoized effective bluesky config until the integrations blob changes', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
const first = await repository.getEffectiveBlueskyConfig();
expect(await repository.getEffectiveBlueskyConfig()).toBe(first);
await repository.setInstanceIntegrationsConfig({bluesky: {client_name: 'Memoized Instance'}});
const updated = await repository.getEffectiveBlueskyConfig();
expect(updated).not.toBe(first);
expect(updated.client_name).toBe('Memoized Instance');
expect(await repository.getEffectiveBlueskyConfig()).toBe(updated);
});
it('recomputes the effective bluesky config after another repository publishes an integrations update', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const reader = createRepository(kvProvider);
const writer = createRepository(kvProvider);
const before = await reader.getEffectiveBlueskyConfig();
expect(await reader.getEffectiveBlueskyConfig()).toBe(before);
await writer.setInstanceIntegrationsConfig({bluesky: {client_name: 'Refreshed Instance'}});
await vi.waitFor(async () => {
expect((await reader.getEffectiveBlueskyConfig()).client_name).toBe('Refreshed Instance');
});
});
it('reports the effective captcha provider as none while the selected pair is incomplete', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setInstanceIntegrationsConfig({
captcha: {
provider: 'turnstile',
hcaptcha_site_key: 'hcaptcha-site-key',
hcaptcha_secret_key: 'hcaptcha-secret-key',
},
});
await expect(repository.getEffectiveCaptchaConfig()).resolves.toMatchObject({
enabled: false,
provider: 'none',
});
await repository.setInstanceIntegrationsConfig({
captcha: {
turnstile_site_key: 'turnstile-site-key',
turnstile_secret_key: 'turnstile-secret-key',
},
});
await expect(repository.getEffectiveCaptchaConfig()).resolves.toMatchObject({
enabled: true,
provider: 'turnstile',
});
});
it('keeps the stored setup state when a branding field is invalid', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(
APP_PUBLIC_CONFIG_KEY,
JSON.stringify({branding: {product_name: 'Kept', icon_url: 42}, setup: {configured: false}}),
);
const config = await repository.getAppPublicConfig();
expect(config.setup.configured).toBe(false);
expect(config.branding.product_name).toBe('Kept');
});
it('keeps valid stored instance policy flags when one field is invalid', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(
INSTANCE_POLICY_CONFIG_KEY,
JSON.stringify({direct_messages_disabled: true, single_community_enabled: true, premium_mode: 'nonsense'}),
);
const policy = await repository.getInstancePolicyConfig();
expect(policy.direct_messages_disabled).toBe(true);
expect(policy.single_community_enabled).toBe(true);
expect(policy.premium_mode).toBe('everyone');
});
it('keeps valid stored integration settings when one field is invalid', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(
INSTANCE_INTEGRATIONS_CONFIG_KEY,
JSON.stringify({bluesky: {client_name: 'Kept Instance', enabled: 'yes'}}),
);
expect((await repository.getEffectiveBlueskyConfig()).client_name).toBe('Kept Instance');
expect((await repository.getInstanceIntegrationsConfig()).bluesky.enabled).toBeNull();
});
it('drops invalid stored SSO allowed domains and keeps the valid ones', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig('sso_allowed_domains', JSON.stringify(['example.com', '[email protected]', 'Kept.ORG']));
expect((await repository.getSsoConfig()).allowedEmailDomains).toEqual(['example.com', 'kept.org']);
});
it('falls back to the default SSO flags when a stored flag is not a boolean', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig('sso_enabled', 'yes');
await repository.setConfig('sso_enforced', 'sometimes');
await repository.setConfig('sso_auto_provision', 'maybe');
const config = await repository.getSsoConfig();
expect(config.enabled).toBe(false);
expect(config.enforced).toBe(false);
expect(config.autoProvision).toBe(true);
});
it('clears an invalid allowed domain list only while SSO is disabled', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
const cleared = await repository.setSsoConfig({enabled: false, allowedEmailDomains: ['[email protected]']});
expect(cleared.allowedEmailDomains).toEqual([]);
await expect(repository.setSsoConfig({enabled: true, allowedEmailDomains: ['[email protected]']})).rejects.toThrow();
});
it('keeps an all-invalid stored allowed domain list non-empty so SSO still rejects every domain', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig('sso_allowed_domains', JSON.stringify(['[email protected]', 'bad/domain']));
const domains = (await repository.getSsoConfig()).allowedEmailDomains;
expect(domains.length).toBeGreaterThan(0);
expect(domains).not.toContain('example.com');
});
it('returns the default voice noise suppression config when the key is absent', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
});
it.each([
{name: 'unparseable text', stored: 'not-json'},
{name: 'a json array', stored: '[]'},
{name: 'out-of-range values', stored: '{"rollout_basis_points":99999}'},
{name: 'an unknown backend', stored: '{"default_backend":"magic"}'},
])('falls back to the default voice noise suppression config for $name', async ({stored}) => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(VOICE_NOISE_SUPPRESSION_CONFIG_KEY, stored);
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
});
it('round-trips a stored voice noise suppression config', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
const config: VoiceNoiseSuppressionConfig = {
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
enabled: true,
config_version: 3,
default_backend: 'rnnoise',
enabled_backends: ['none', 'standard', 'rnnoise'],
allow_user_override: false,
rollout_basis_points: 2500,
rollout_salt: 'voice-ns-v2',
included_user_ids: ['1400000000000000001'],
excluded_user_ids: ['1400000000000000002'],
guild_overrides: [{guild_id: '2400000000000000001', backend: 'rnnoise'}],
suppression_strength: 55,
};
await repository.setVoiceNoiseSuppressionConfig(config);
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual(config);
});
it('fills newly added voice noise suppression fields from the schema defaults', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(
VOICE_NOISE_SUPPRESSION_CONFIG_KEY,
JSON.stringify({enabled: true, config_version: 2, rollout_basis_points: 1000}),
);
await expect(repository.getVoiceNoiseSuppressionConfig()).resolves.toEqual({
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
enabled: true,
config_version: 2,
rollout_basis_points: 1000,
});
});
it('returns the default experiment delivery config when the key is absent', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(DEFAULT_EXPERIMENT_DELIVERY_CONFIG);
});
it.each([
{name: 'unparseable text', stored: 'not-json'},
{name: 'a json array', stored: '[]'},
{name: 'an out-of-range poll interval', stored: '{"poll_interval_seconds":1}'},
{name: 'an out-of-range jitter', stored: '{"poll_jitter_percent":99}'},
{name: 'a non-numeric poll interval', stored: '{"poll_interval_seconds":"often"}'},
])('falls back to the default experiment delivery config for $name', async ({stored}) => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, stored);
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(DEFAULT_EXPERIMENT_DELIVERY_CONFIG);
});
it('round-trips a stored experiment delivery config', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
const config: ExperimentDeliveryConfig = {poll_interval_seconds: 900, poll_jitter_percent: 0};
await repository.setExperimentDeliveryConfig(config);
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual(config);
});
it('fills missing experiment delivery fields from the schema defaults', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
await repository.setConfig(EXPERIMENT_DELIVERY_CONFIG_KEY, JSON.stringify({poll_interval_seconds: 3600}));
await expect(repository.getExperimentDeliveryConfig()).resolves.toEqual({
...DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
poll_interval_seconds: 3600,
});
});
it('publishes a refresh so another repository observes the voice noise suppression config', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const reader = createRepository(kvProvider);
const writer = createRepository(kvProvider);
await expect(reader.getVoiceNoiseSuppressionConfig()).resolves.toEqual(DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG);
await writer.setVoiceNoiseSuppressionConfig({
...DEFAULT_VOICE_NOISE_SUPPRESSION_CONFIG,
enabled: true,
config_version: 1,
});
await vi.waitFor(async () => {
expect(await reader.getVoiceNoiseSuppressionConfig()).toMatchObject({enabled: true, config_version: 1});
});
});
it('uses the registration URL id as the admin-visible registration code', async () => {
const executor = new CountingInMemoryCassandraQueryExecutor();
setCassandraQueryExecutorForTesting(executor);
const kvProvider = new MockKVProvider();
const repository = createRepository(kvProvider);
const created = await repository.createRegistrationUrl({
label: 'Support invite',
createdByUserId: '1500000000000000000',
expiresAt: null,
maxUses: null,
approvalRequired: false,
});
expect(created.code).toBe(created.registrationUrl.id);
expect(created.registrationUrl).not.toHaveProperty('code_hash');
await expect(repository.resolveRegistrationUrlCode(created.registrationUrl.id)).resolves.toMatchObject({
id: created.registrationUrl.id,
});
});
});