mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-12 05:28:28 +09:00
214 lines
6.9 KiB
TypeScript
214 lines
6.9 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {buildAPIConfigFromMaster, buildAPIServerOptions} from '@app/api/Config';
|
|
import {loadConfig, resetConfig} from '@fluxer/config/src/ConfigLoader';
|
|
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
|
import {createServer} from '@fluxer/hono/src/Server';
|
|
import {Hono} from 'hono';
|
|
import {afterAll, afterEach, beforeAll, describe, expect, it, test, vi} from 'vitest';
|
|
|
|
interface ListeningServer {
|
|
close: (callback: () => void) => void;
|
|
headersTimeout: number;
|
|
requestTimeout: number;
|
|
}
|
|
|
|
const servers: Array<ListeningServer> = [];
|
|
|
|
async function listenWithEnv(env: Record<string, string> = {}): Promise<ListeningServer> {
|
|
for (const [key, value] of Object.entries({FLUXER_API_PORT: '0', ...env})) {
|
|
vi.stubEnv(key, value);
|
|
}
|
|
resetConfig();
|
|
const config = buildAPIConfigFromMaster(await loadConfig());
|
|
const server = createServer(new Hono(), buildAPIServerOptions(config)) as unknown as ListeningServer;
|
|
servers.push(server);
|
|
return server;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
|
|
vi.unstubAllEnvs();
|
|
resetConfig();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await loadConfig();
|
|
});
|
|
|
|
describe('buildAPIServerOptions', () => {
|
|
test('starts the api on the shipped header and request timeouts', async () => {
|
|
const server = await listenWithEnv();
|
|
expect(server.headersTimeout).toBe(30_000);
|
|
expect(server.requestTimeout).toBe(120_000);
|
|
});
|
|
|
|
test('carries the operator header timeout from the environment into the server', async () => {
|
|
const server = await listenWithEnv({FLUXER_API_HEADERS_TIMEOUT_MS: '45000'});
|
|
expect(server.headersTimeout).toBe(45_000);
|
|
expect(server.requestTimeout).toBe(120_000);
|
|
});
|
|
|
|
test('carries the operator request timeout from the environment into the server', async () => {
|
|
const server = await listenWithEnv({FLUXER_API_REQUEST_TIMEOUT_MS: '600000'});
|
|
expect(server.headersTimeout).toBe(30_000);
|
|
expect(server.requestTimeout).toBe(600_000);
|
|
});
|
|
|
|
test('clamps a header timeout set above the request timeout', async () => {
|
|
const server = await listenWithEnv({
|
|
FLUXER_API_HEADERS_TIMEOUT_MS: '90000',
|
|
FLUXER_API_REQUEST_TIMEOUT_MS: '45000',
|
|
});
|
|
expect(server.requestTimeout).toBe(45_000);
|
|
expect(server.headersTimeout).toBe(45_000);
|
|
});
|
|
});
|
|
|
|
function withUploadRelaySecret(master: MasterConfig, secretBase64: string): MasterConfig {
|
|
return {
|
|
...master,
|
|
services: {
|
|
...master.services,
|
|
media_proxy: {
|
|
...master.services.media_proxy,
|
|
upload_relay: {
|
|
...master.services.media_proxy.upload_relay,
|
|
secret_base64: secretBase64,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function withStripeLegacyPrices(
|
|
master: MasterConfig,
|
|
legacyPrices: Record<string, Array<string> | undefined> | undefined,
|
|
): MasterConfig {
|
|
return {
|
|
...master,
|
|
integrations: {
|
|
...master.integrations,
|
|
stripe: {
|
|
...master.integrations.stripe,
|
|
legacy_prices: legacyPrices,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('buildAPIConfigFromMaster upload relay secret', () => {
|
|
let master: MasterConfig;
|
|
beforeAll(async () => {
|
|
master = await loadConfig();
|
|
});
|
|
|
|
it('refuses to build without FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64', () => {
|
|
expect(() => buildAPIConfigFromMaster(withUploadRelaySecret(master, ''))).toThrow(
|
|
/FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64/,
|
|
);
|
|
});
|
|
|
|
it('refuses a secret that decodes to fewer than 32 bytes', () => {
|
|
const secret = Buffer.alloc(16, 7).toString('base64');
|
|
expect(() => buildAPIConfigFromMaster(withUploadRelaySecret(master, secret))).toThrow(/at least 32 bytes/);
|
|
});
|
|
|
|
it('accepts a secret that decodes to 32 bytes', () => {
|
|
const secret = Buffer.alloc(32, 7).toString('base64');
|
|
expect(
|
|
buildAPIConfigFromMaster(withUploadRelaySecret(master, secret)).mediaProxy.uploadRelay.relaySecretBase64,
|
|
).toBe(secret);
|
|
});
|
|
|
|
it('reads the relay secret from the loaded config rather than the environment', () => {
|
|
expect(buildAPIConfigFromMaster(master).mediaProxy.uploadRelay.relaySecretBase64).toBe(
|
|
master.services.media_proxy.upload_relay.secret_base64,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('buildAPIConfigFromMaster stripe legacy prices', () => {
|
|
let master: MasterConfig;
|
|
beforeAll(async () => {
|
|
master = await loadConfig();
|
|
});
|
|
|
|
it('carries the retired stripe price map from master config onto the api config', () => {
|
|
const legacyPrices = {
|
|
monthly_brl: ['price_retired_monthly_brl'],
|
|
yearly_brl: ['price_retired_yearly_brl_a', 'price_retired_yearly_brl_b'],
|
|
monthly_try: ['price_1TMYpdFPC94Os7FdZVRx98Up'],
|
|
};
|
|
expect(buildAPIConfigFromMaster(withStripeLegacyPrices(master, legacyPrices)).stripe.legacyPrices).toEqual(
|
|
legacyPrices,
|
|
);
|
|
});
|
|
|
|
it('carries the retired price map even when no live prices are configured', () => {
|
|
const withoutPrices: MasterConfig = {
|
|
...master,
|
|
integrations: {
|
|
...master.integrations,
|
|
stripe: {
|
|
...master.integrations.stripe,
|
|
prices: undefined,
|
|
legacy_prices: {monthly_try: ['price_1TMYpdFPC94Os7FdZVRx98Up']},
|
|
},
|
|
},
|
|
};
|
|
const config = buildAPIConfigFromMaster(withoutPrices);
|
|
expect(config.stripe.prices).toBeUndefined();
|
|
expect(config.stripe.legacyPrices).toEqual({monthly_try: ['price_1TMYpdFPC94Os7FdZVRx98Up']});
|
|
});
|
|
|
|
it('leaves the retired price map undefined when master config does not set one', () => {
|
|
expect(buildAPIConfigFromMaster(withStripeLegacyPrices(master, undefined)).stripe.legacyPrices).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
function withOptionalOutboundLookups(
|
|
master: MasterConfig,
|
|
selfHosted: boolean,
|
|
overrides: {breachedPasswordCheck?: boolean} = {},
|
|
): MasterConfig {
|
|
return {
|
|
...master,
|
|
integrations: {
|
|
...master.integrations,
|
|
breached_password_check: {enabled: overrides.breachedPasswordCheck},
|
|
},
|
|
instance: {
|
|
...master.instance,
|
|
self_hosted: selfHosted,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('buildAPIConfigFromMaster optional outbound lookups', () => {
|
|
let master: MasterConfig;
|
|
beforeAll(async () => {
|
|
master = await loadConfig();
|
|
});
|
|
|
|
it('keeps the lookup on when the instance is not self-hosted', () => {
|
|
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, false));
|
|
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
|
});
|
|
|
|
it('leaves the lookup off on a self-hosted instance', () => {
|
|
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, true));
|
|
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
|
});
|
|
|
|
it('lets a self-hosted operator switch the lookup on', () => {
|
|
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, true, {breachedPasswordCheck: true}));
|
|
expect(config.breachedPasswordCheck.enabled).toBe(true);
|
|
});
|
|
|
|
it('lets an operator switch the lookup off when the instance is not self-hosted', () => {
|
|
const config = buildAPIConfigFromMaster(withOptionalOutboundLookups(master, false, {breachedPasswordCheck: false}));
|
|
expect(config.breachedPasswordCheck.enabled).toBe(false);
|
|
});
|
|
});
|