mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
379 lines
17 KiB
TypeScript
379 lines
17 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import type {TestAccount} from '@app/api/auth/tests/AuthTestUtils';
|
|
import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
|
|
import {getConfig} from '@app/api/Config';
|
|
import {getCachedInstancePremiumMode, setCachedInstancePremiumMode} from '@app/api/limits/InstancePremiumModeCache';
|
|
import {getAdminRepository, getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
|
import {getStoredBillingConfig, setStoredBillingConfig} from '@app/api/stripe/BillingConfigCache';
|
|
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
|
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
|
import {createBuilder} from '@app/api/test/TestRequestBuilder';
|
|
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
|
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
|
import type {InstanceConfigResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
|
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
|
|
const OPERATOR_SECRET_KEY = 'sk_test_operator_secret_value';
|
|
const OPERATOR_WEBHOOK_SECRET = 'whsec_operator_secret_value';
|
|
|
|
interface GlobalState {
|
|
selfHosted: boolean;
|
|
premiumMode: ReturnType<typeof getCachedInstancePremiumMode>;
|
|
storedBilling: ReturnType<typeof getStoredBillingConfig>;
|
|
}
|
|
|
|
function captureGlobalState(): GlobalState {
|
|
return {
|
|
selfHosted: getConfig().instance.selfHosted,
|
|
premiumMode: getCachedInstancePremiumMode(),
|
|
storedBilling: getStoredBillingConfig(),
|
|
};
|
|
}
|
|
|
|
function restoreGlobalState(state: GlobalState): void {
|
|
getConfig().instance.selfHosted = state.selfHosted;
|
|
setCachedInstancePremiumMode(state.premiumMode);
|
|
setStoredBillingConfig(state.storedBilling);
|
|
}
|
|
|
|
function useInstanceHarness(selfHosted: boolean) {
|
|
const context: {harness: ApiTestHarness} = {harness: undefined as unknown as ApiTestHarness};
|
|
let original: GlobalState;
|
|
beforeAll(async () => {
|
|
original = captureGlobalState();
|
|
getConfig().instance.selfHosted = selfHosted;
|
|
context.harness = await createApiTestHarness();
|
|
});
|
|
beforeEach(async () => {
|
|
await context.harness.reset();
|
|
setStoredBillingConfig(null);
|
|
setCachedInstancePremiumMode('everyone');
|
|
});
|
|
afterAll(async () => {
|
|
await context.harness.shutdown();
|
|
restoreGlobalState(original);
|
|
});
|
|
return context;
|
|
}
|
|
|
|
async function createAdmin(harness: ApiTestHarness): Promise<TestAccount> {
|
|
return await setUserACLs(harness, await createTestAccount(harness), [
|
|
AdminACLs.AUTHENTICATE,
|
|
AdminACLs.INSTANCE_CONFIG_VIEW,
|
|
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
|
]);
|
|
}
|
|
|
|
function patchConfig(harness: ApiTestHarness, admin: TestAccount, body: Record<string, unknown>) {
|
|
return createBuilder<InstanceConfigResponse>(harness, admin.token).patch('/admin/instance/config').body(body);
|
|
}
|
|
|
|
const OPERATOR_BILLING = {
|
|
enabled: true,
|
|
stripe_secret_key: OPERATOR_SECRET_KEY,
|
|
stripe_webhook_secret: OPERATOR_WEBHOOK_SECRET,
|
|
default_currency: 'GBP',
|
|
prices: {
|
|
GBP: {
|
|
monthly: 'price_monthlygbp',
|
|
yearly: 'price_yearlygbp',
|
|
gift_1_month: 'price_gift1monthgbp',
|
|
gift_1_year: 'price_gift1yeargbp',
|
|
},
|
|
},
|
|
country_currencies: {GB: 'GBP'},
|
|
legacy_prices: {monthly_GBP: ['price_oldmonthlygbp']},
|
|
};
|
|
|
|
describe('instance config billing on a self-hosted instance', () => {
|
|
const context = useInstanceHarness(true);
|
|
|
|
it('rejects enabling billing while the premium mode is everyone', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: {enabled: true}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
await patchConfig(context.harness, admin, {billing: {enabled: true}, policy: {premium_mode: 'everyone'}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.billing.enabled).toBeNull();
|
|
expect(current.policy.premium_mode).toBe('everyone');
|
|
});
|
|
|
|
it('saves the environment fallback in everyone mode even when the environment enables billing', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const originalEnabled = getConfig().stripe.enabled;
|
|
getConfig().stripe.enabled = true;
|
|
try {
|
|
const updated = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: 'Gold'}},
|
|
billing: {enabled: null},
|
|
}).execute();
|
|
expect(updated.billing.enabled).toBeNull();
|
|
expect(updated.billing.billing_active).toBe(false);
|
|
expect(updated.app_public.branding.premium_product_name).toBe('Gold');
|
|
await patchConfig(context.harness, admin, {policy: {premium_mode: 'everyone'}}).execute();
|
|
} finally {
|
|
getConfig().stripe.enabled = originalEnabled;
|
|
}
|
|
});
|
|
|
|
it('enables billing and mirror mode from one request and redacts the secrets', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const updated = await patchConfig(context.harness, admin, {
|
|
billing: OPERATOR_BILLING,
|
|
policy: {premium_mode: 'mirror'},
|
|
}).execute();
|
|
expect(updated.policy.premium_mode).toBe('mirror');
|
|
expect(updated.billing).toMatchObject({
|
|
enabled: true,
|
|
effective_enabled: true,
|
|
stripe_secret_key_set: true,
|
|
stripe_webhook_secret_set: true,
|
|
default_currency: 'GBP',
|
|
prices: OPERATOR_BILLING.prices,
|
|
country_currencies: {GB: 'GBP'},
|
|
legacy_prices: {monthly_GBP: ['price_oldmonthlygbp']},
|
|
billing_active: true,
|
|
catalog_mode: 'operator',
|
|
});
|
|
expect(updated.billing.webhook_url).toMatch(/\/stripe\/webhook$/);
|
|
const {text} = await createBuilder(context.harness, admin.token).get('/admin/instance/config').executeRaw();
|
|
expect(text).not.toContain(OPERATOR_SECRET_KEY);
|
|
expect(text).not.toContain(OPERATOR_WEBHOOK_SECRET);
|
|
expect(JSON.parse(text).billing).toMatchObject({stripe_secret_key_set: true, billing_active: true});
|
|
});
|
|
|
|
it('rejects switching the premium mode to everyone while billing is enabled', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: OPERATOR_BILLING, policy: {premium_mode: 'mirror'}}).execute();
|
|
await patchConfig(context.harness, admin, {policy: {premium_mode: 'everyone'}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
const unchanged = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(unchanged.policy.premium_mode).toBe('mirror');
|
|
const switched = await patchConfig(context.harness, admin, {
|
|
billing: {enabled: false},
|
|
policy: {premium_mode: 'everyone'},
|
|
}).execute();
|
|
expect(switched.policy.premium_mode).toBe('everyone');
|
|
expect(switched.billing).toMatchObject({enabled: false, effective_enabled: false, billing_active: false});
|
|
});
|
|
|
|
it('clears secrets and the operator catalog with null', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: OPERATOR_BILLING, policy: {premium_mode: 'mirror'}}).execute();
|
|
const kept = await patchConfig(context.harness, admin, {billing: {default_currency: 'GBP'}}).execute();
|
|
expect(kept.billing.prices).toEqual(OPERATOR_BILLING.prices);
|
|
const cleared = await patchConfig(context.harness, admin, {
|
|
billing: {enabled: true, stripe_webhook_secret: null, prices: null, legacy_prices: null},
|
|
}).execute();
|
|
expect(cleared.billing).toMatchObject({
|
|
enabled: true,
|
|
prices: null,
|
|
legacy_prices: null,
|
|
catalog_mode: 'env',
|
|
stripe_secret_key_set: true,
|
|
});
|
|
});
|
|
|
|
it('rejects malformed billing input', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
for (const billing of [
|
|
{prices: {gbp: {monthly: 'price_x'}}},
|
|
{prices: {GBP: {monthly: 'not_a_price'}}},
|
|
{country_currencies: {GBR: 'GBP'}},
|
|
{legacy_prices: {weekly_GBP: ['price_x']}},
|
|
{default_currency: 'pounds'},
|
|
]) {
|
|
await patchConfig(context.harness, admin, {billing})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
}
|
|
});
|
|
|
|
it('clears the premium name and info URL when both are sent as null', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: 'Gold', premium_info_url: 'https://example.com/gold'}},
|
|
}).execute();
|
|
const reset = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: null, premium_info_url: null}},
|
|
}).execute();
|
|
expect(reset.app_public.branding).toMatchObject({premium_product_name: 'Premium', premium_info_url: null});
|
|
await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: 'Gold', premium_info_url: 'https://example.com/gold'}},
|
|
}).execute();
|
|
const resetWithBilling = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: null, premium_info_url: null}},
|
|
billing: {enabled: false},
|
|
}).execute();
|
|
expect(resetWithBilling.app_public.branding).toMatchObject({
|
|
premium_product_name: 'Premium',
|
|
premium_info_url: null,
|
|
});
|
|
});
|
|
|
|
it('applies billing sections that only hold nulls', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: OPERATOR_BILLING, policy: {premium_mode: 'mirror'}}).execute();
|
|
const envCatalog = await patchConfig(context.harness, admin, {billing: {prices: null}}).execute();
|
|
expect(envCatalog.billing).toMatchObject({prices: null, catalog_mode: 'env', enabled: true});
|
|
const followsEnv = await patchConfig(context.harness, admin, {billing: {enabled: null}}).execute();
|
|
expect(followsEnv.billing).toMatchObject({enabled: null, effective_enabled: getConfig().stripe.enabled});
|
|
});
|
|
|
|
it('applies the policy before billing so a failed policy change stores no billing', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {app_public: {setup: {configured: true}}}).execute();
|
|
await patchConfig(context.harness, admin, {
|
|
billing: {enabled: true},
|
|
policy: {premium_mode: 'mirror', single_community_enabled: true},
|
|
})
|
|
.expect(HTTP_STATUS.BAD_REQUEST)
|
|
.execute();
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.policy.premium_mode).toBe('everyone');
|
|
expect(current.billing.enabled).toBeNull();
|
|
});
|
|
|
|
it('re-checks the stored premium mode right before the billing write', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const repository = getInstanceConfigRepository();
|
|
const stored = await repository.getInstancePolicyConfig();
|
|
const spy = vi.spyOn(repository, 'getInstancePolicyConfig').mockResolvedValue({...stored, premium_mode: 'mirror'});
|
|
try {
|
|
await patchConfig(context.harness, admin, {billing: {enabled: true}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.billing.enabled).toBeNull();
|
|
});
|
|
|
|
it('re-checks the stored billing right before the premium mode write', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: OPERATOR_BILLING, policy: {premium_mode: 'mirror'}}).execute();
|
|
const cached = getStoredBillingConfig();
|
|
expect(cached?.enabled).toBe(true);
|
|
setStoredBillingConfig(cached === null ? null : {...cached, enabled: false});
|
|
await patchConfig(context.harness, admin, {policy: {premium_mode: 'everyone'}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.policy.premium_mode).toBe('mirror');
|
|
expect(current.billing.enabled).toBe(true);
|
|
});
|
|
|
|
it('records the billing section in the audit log without secrets', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
await patchConfig(context.harness, admin, {billing: OPERATOR_BILLING, policy: {premium_mode: 'mirror'}}).execute();
|
|
const logs = await getAdminRepository().listAllAuditLogsPaginated(100000);
|
|
const update = logs.find((log) => log.action === 'update_instance_config');
|
|
expect(update?.metadata.get('sections')).toBe('billing,policy');
|
|
const serialized = JSON.stringify(logs.map((log) => [...log.metadata.entries()]));
|
|
expect(serialized).not.toContain(OPERATOR_SECRET_KEY);
|
|
expect(serialized).not.toContain(OPERATOR_WEBHOOK_SECRET);
|
|
});
|
|
|
|
it('stores the premium name and info URL and resets the name to the self-hosted default', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const initial = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(initial.app_public.branding).toMatchObject({premium_product_name: 'Premium', premium_info_url: null});
|
|
const named = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {premium_product_name: 'Gold', premium_info_url: 'https://example.com/gold'}},
|
|
}).execute();
|
|
expect(named.app_public.branding).toMatchObject({
|
|
premium_product_name: 'Gold',
|
|
premium_info_url: 'https://example.com/gold',
|
|
});
|
|
const renamedProduct = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {product_name: 'Example Chat'}},
|
|
}).execute();
|
|
expect(renamedProduct.app_public.branding.premium_product_name).toBe('Gold');
|
|
const reset = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {product_name: 'Example Chat', premium_product_name: null, premium_info_url: null}},
|
|
}).execute();
|
|
expect(reset.app_public.branding).toMatchObject({premium_product_name: 'Premium', premium_info_url: null});
|
|
await patchConfig(context.harness, admin, {app_public: {branding: {premium_info_url: 'javascript:alert(1)'}}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
});
|
|
});
|
|
|
|
describe('instance config billing on a hosted instance', () => {
|
|
const context = useInstanceHarness(false);
|
|
|
|
it('keeps the Plutonium default and env billing', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.app_public.branding.premium_product_name).toBe('Plutonium');
|
|
expect(current.billing).toMatchObject({enabled: null, catalog_mode: 'env', stripe_secret_key_set: true});
|
|
const updated = await patchConfig(context.harness, admin, {policy: {premium_mode: 'everyone'}}).execute();
|
|
expect(updated.policy.premium_mode).toBe('everyone');
|
|
});
|
|
|
|
it('rejects the billing section and the premium branding fields', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
for (const body of [
|
|
{billing: {enabled: true}},
|
|
{billing: {enabled: null}},
|
|
{billing: {prices: null}},
|
|
{billing: OPERATOR_BILLING},
|
|
{app_public: {branding: {premium_product_name: 'Gold'}}},
|
|
{app_public: {branding: {premium_info_url: 'https://example.com/gold'}}},
|
|
{app_public: {branding: {premium_product_name: null, premium_info_url: null}}},
|
|
{app_public: {branding: {product_name: 'Fluxer', premium_product_name: 'Gold'}}},
|
|
]) {
|
|
await patchConfig(context.harness, admin, body)
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
}
|
|
const current = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
|
.get('/admin/instance/config')
|
|
.execute();
|
|
expect(current.billing).toMatchObject({enabled: null, catalog_mode: 'env', prices: null});
|
|
expect(current.app_public.branding).toMatchObject({premium_product_name: 'Plutonium', premium_info_url: null});
|
|
expect(getStoredBillingConfig()?.prices ?? null).toBeNull();
|
|
});
|
|
|
|
it('still accepts other branding fields', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
const updated = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {theme_color: '#123456'}},
|
|
}).execute();
|
|
expect(updated.app_public.branding.theme_color).toBe('#123456');
|
|
});
|
|
|
|
it('rejects a theme colour that is not hex', async () => {
|
|
const admin = await createAdmin(context.harness);
|
|
for (const theme_color of ['rgb(123, 44, 228)', 'rebeccapurple', '#12345']) {
|
|
await patchConfig(context.harness, admin, {app_public: {branding: {theme_color}}})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
|
.execute();
|
|
}
|
|
const cleared = await patchConfig(context.harness, admin, {
|
|
app_public: {branding: {theme_color: null}},
|
|
}).execute();
|
|
expect(cleared.app_public.branding.theme_color).toBeNull();
|
|
});
|
|
});
|