mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
fix(admin): save premium limits and model changes (#3386)
This commit is contained in:
1 parent
7d2c51a57f
commit
6acd7f3d22
18 files changed
+1035
-59
No files matched your search
@@ -0,0 +1,73 @@
|
||||
// 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 {getInstanceConfigRepository, getLimitConfigService} from '@app/api/middleware/ServiceSingletons';
|
||||
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 {MAX_BIO_LENGTH} from '@fluxer/constants/src/LimitConstants';
|
||||
import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type LimitConfigResponse = z.infer<typeof LimitConfigGetResponse>;
|
||||
|
||||
describe('limit config allowed ranges', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness.shutdown();
|
||||
});
|
||||
|
||||
async function createAdmin(): Promise<TestAccount> {
|
||||
return await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_LIMIT_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_LIMIT_CONFIG_UPDATE,
|
||||
]);
|
||||
}
|
||||
|
||||
function vipRule(maxBioLength: number) {
|
||||
return {id: 'vip', filters: {traits: ['vip']}, limits: {max_bio_length: maxBioLength}};
|
||||
}
|
||||
|
||||
it('refuses a bio length above what a profile can hold', async () => {
|
||||
const admin = await createAdmin();
|
||||
await createBuilder(harness, admin.token)
|
||||
.put('/admin/limit-config')
|
||||
.body({limit_config: {traitDefinitions: ['vip'], rules: [vipRule(MAX_BIO_LENGTH + 1)]}})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
||||
.execute();
|
||||
const saved = await createBuilder<LimitConfigResponse>(harness, admin.token)
|
||||
.put('/admin/limit-config')
|
||||
.body({limit_config: {traitDefinitions: ['vip'], rules: [vipRule(MAX_BIO_LENGTH)]}})
|
||||
.execute();
|
||||
expect(saved.limit_config.rules.find((rule) => rule.id === 'vip')?.limits).toEqual({
|
||||
max_bio_length: MAX_BIO_LENGTH,
|
||||
});
|
||||
expect(saved.metadata.max_bio_length).toMatchObject({min: 0, max: MAX_BIO_LENGTH});
|
||||
});
|
||||
|
||||
it('reads a stored bio length above the profile maximum as the maximum', async () => {
|
||||
const admin = await createAdmin();
|
||||
await getInstanceConfigRepository().setLimitConfig({traitDefinitions: ['vip'], rules: [vipRule(1000)]});
|
||||
await getLimitConfigService().refreshCache();
|
||||
const response = await createBuilder<LimitConfigResponse>(harness, admin.token)
|
||||
.get('/admin/limit-config')
|
||||
.execute();
|
||||
expect(response.limit_config.rules.find((rule) => rule.id === 'vip')?.limits).toEqual({
|
||||
max_bio_length: MAX_BIO_LENGTH,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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 {getInstanceConfigRepository, getLimitConfigService} from '@app/api/middleware/ServiceSingletons';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {fetchUserMe, grantPremium} from '@app/api/user/tests/UserTestUtils';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants';
|
||||
import {DEFAULT_RESTRICTED_LIMITS, DEFAULT_STOCK_LIMITS} from '@fluxer/limits/src/LimitDefaults';
|
||||
import {expandWireFormat} from '@fluxer/limits/src/LimitDiffer';
|
||||
import {resolveLimits} from '@fluxer/limits/src/LimitResolver';
|
||||
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
|
||||
import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {WellKnownFluxerResponse} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import type {z} from 'zod';
|
||||
|
||||
type LimitConfigResponse = z.infer<typeof LimitConfigGetResponse>;
|
||||
|
||||
const MODE_KEYS = LIMIT_KEYS.filter((key) => DEFAULT_STOCK_LIMITS[key] !== DEFAULT_RESTRICTED_LIMITS[key]);
|
||||
|
||||
describe('limit config across premium model switches on a self-hosted instance', () => {
|
||||
let harness: ApiTestHarness;
|
||||
let originalSelfHosted: boolean;
|
||||
let originalPremiumMode: ReturnType<typeof getCachedInstancePremiumMode>;
|
||||
|
||||
beforeAll(async () => {
|
||||
originalSelfHosted = getConfig().instance.selfHosted;
|
||||
originalPremiumMode = getCachedInstancePremiumMode();
|
||||
getConfig().instance.selfHosted = true;
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness.shutdown();
|
||||
getConfig().instance.selfHosted = originalSelfHosted;
|
||||
setCachedInstancePremiumMode(originalPremiumMode);
|
||||
});
|
||||
|
||||
async function createAdmin(): Promise<TestAccount> {
|
||||
return await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
AdminACLs.INSTANCE_LIMIT_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_LIMIT_CONFIG_UPDATE,
|
||||
]);
|
||||
}
|
||||
|
||||
async function setPremiumMode(admin: TestAccount, premiumMode: 'mirror' | 'everyone'): Promise<void> {
|
||||
await createBuilder(harness, admin.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({policy: {premium_mode: premiumMode}})
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function getLimitConfig(admin: TestAccount): Promise<LimitConfigResponse> {
|
||||
return await createBuilder<LimitConfigResponse>(harness, admin.token).get('/admin/limit-config').execute();
|
||||
}
|
||||
|
||||
async function saveRule(admin: TestAccount, ruleId: string, overrides: Record<string, number>): Promise<void> {
|
||||
const {limit_config: config} = await getLimitConfig(admin);
|
||||
await createBuilder(harness, admin.token)
|
||||
.put('/admin/limit-config')
|
||||
.body({
|
||||
limit_config: {
|
||||
traitDefinitions: config.traitDefinitions,
|
||||
rules: config.rules.map(({modifiedFields: _modifiedFields, ...rule}) =>
|
||||
rule.id === ruleId ? {...rule, limits: {...rule.limits, ...overrides}} : rule,
|
||||
),
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
function saveDefaultRule(admin: TestAccount, overrides: Record<string, number>): Promise<void> {
|
||||
return saveRule(admin, 'default', overrides);
|
||||
}
|
||||
|
||||
function findRule(response: LimitConfigResponse, ruleId: string) {
|
||||
const rule = response.limit_config.rules.find((candidate) => candidate.id === ruleId);
|
||||
expect(rule).toBeDefined();
|
||||
return rule!;
|
||||
}
|
||||
|
||||
function defaultRule(response: LimitConfigResponse) {
|
||||
return findRule(response, 'default');
|
||||
}
|
||||
|
||||
async function storeLegacyLimitConfig(config: LimitConfigSnapshot): Promise<void> {
|
||||
await getInstanceConfigRepository().setLimitConfig(config);
|
||||
await getLimitConfigService().refreshCache();
|
||||
}
|
||||
|
||||
async function changeTag(account: TestAccount, discriminator: string): Promise<number> {
|
||||
const {response} = await createBuilder(harness, account.token)
|
||||
.patch('/users/@me')
|
||||
.body({discriminator, password: account.password})
|
||||
.executeRaw();
|
||||
return response.status;
|
||||
}
|
||||
|
||||
it('gives everyone premium limits after a save in mirror mode and keeps edited values', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
await saveDefaultRule(admin, {max_bio_length: 300});
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
const rule = defaultRule(await getLimitConfig(admin));
|
||||
expect(rule.limits).toEqual({...DEFAULT_STOCK_LIMITS, max_bio_length: 300});
|
||||
expect(rule.modifiedFields).toEqual(['max_bio_length']);
|
||||
const discovery = await createBuilderWithoutAuth<WellKnownFluxerResponse>(harness)
|
||||
.get('/.well-known/fluxer')
|
||||
.execute();
|
||||
expect(discovery.limits.rules.find((candidate) => candidate.id === 'default')?.overrides).toMatchObject({
|
||||
feature_custom_discriminator: 1,
|
||||
max_guilds: DEFAULT_STOCK_LIMITS.max_guilds,
|
||||
});
|
||||
});
|
||||
|
||||
it('restores free limits after a save in everyone mode', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
await saveDefaultRule(admin, {});
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const response = await getLimitConfig(admin);
|
||||
expect(defaultRule(response).limits).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
expect(response.limit_config.rules.find((rule) => rule.id === 'premium')?.limits).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
});
|
||||
|
||||
it('stores only the edited values', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
await saveDefaultRule(admin, {max_bio_length: 300});
|
||||
const {config: stored} = await getInstanceConfigRepository().readLimitConfigInputs();
|
||||
expect(stored?.overridesOnly).toBe(true);
|
||||
expect(stored?.rules).toEqual([
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {}},
|
||||
{id: 'default', limits: {max_bio_length: 300}},
|
||||
{id: 'very_large_guild', filters: {guildFeatures: ['VERY_LARGE_GUILD']}, limits: {}},
|
||||
]);
|
||||
});
|
||||
|
||||
it('repairs free limits stored in full before an earlier switch to everyone', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
await storeLegacyLimitConfig({
|
||||
traitDefinitions: ['premium'],
|
||||
rules: [
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {...DEFAULT_STOCK_LIMITS}},
|
||||
{
|
||||
id: 'default',
|
||||
limits: {...DEFAULT_RESTRICTED_LIMITS, max_bio_length: 300},
|
||||
modifiedFields: ['max_bio_length'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const rule = defaultRule(await getLimitConfig(admin));
|
||||
expect(rule.limits).toEqual({...DEFAULT_STOCK_LIMITS, max_bio_length: 300});
|
||||
expect(rule.modifiedFields).toEqual(['max_bio_length']);
|
||||
});
|
||||
|
||||
it('repairs premium limits stored in full before an earlier switch to mirror', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
await storeLegacyLimitConfig({
|
||||
traitDefinitions: [],
|
||||
rules: [{id: 'default', limits: {...DEFAULT_STOCK_LIMITS}}],
|
||||
});
|
||||
const response = await getLimitConfig(admin);
|
||||
expect(defaultRule(response).limits).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
expect(defaultRule(response).modifiedFields).toBeUndefined();
|
||||
});
|
||||
|
||||
it('repairs free limits saved again after an earlier switch to everyone', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
await storeLegacyLimitConfig({
|
||||
traitDefinitions: [],
|
||||
rules: [{id: 'default', limits: {...DEFAULT_RESTRICTED_LIMITS}, modifiedFields: MODE_KEYS}],
|
||||
});
|
||||
const rule = defaultRule(await getLimitConfig(admin));
|
||||
expect(rule.limits).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
expect(rule.modifiedFields).toBeUndefined();
|
||||
const member = await createTestAccount(harness);
|
||||
expect(await changeTag(member, '4242')).toBe(200);
|
||||
await saveDefaultRule(admin, {max_bio_length: 300});
|
||||
const {config: stored} = await getInstanceConfigRepository().readLimitConfigInputs();
|
||||
expect(stored?.rules.find((candidate) => candidate.id === 'default')).toEqual({
|
||||
id: 'default',
|
||||
limits: {max_bio_length: 300},
|
||||
});
|
||||
});
|
||||
|
||||
it('repairs premium limits saved again after an earlier switch to mirror', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
await storeLegacyLimitConfig({
|
||||
traitDefinitions: [],
|
||||
rules: [{id: 'default', limits: {...DEFAULT_STOCK_LIMITS}, modifiedFields: MODE_KEYS}],
|
||||
});
|
||||
const response = await getLimitConfig(admin);
|
||||
expect(defaultRule(response).limits).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
expect(findRule(response, 'premium').limits).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
expect(response.limit_config.traitDefinitions).toEqual(['premium']);
|
||||
});
|
||||
|
||||
it('reads limits stored in full under the model that is being left', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
await storeLegacyLimitConfig({
|
||||
traitDefinitions: ['premium'],
|
||||
rules: [
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {...DEFAULT_STOCK_LIMITS}},
|
||||
{id: 'default', limits: {...DEFAULT_RESTRICTED_LIMITS, max_guilds: 150}},
|
||||
],
|
||||
});
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
expect(defaultRule(await getLimitConfig(admin)).limits).toEqual({...DEFAULT_STOCK_LIMITS, max_guilds: 150});
|
||||
const {config: stored} = await getInstanceConfigRepository().readLimitConfigInputs();
|
||||
expect(stored?.rules).toEqual([
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {}},
|
||||
{id: 'default', limits: {max_guilds: 150}},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps edits that match the other model across a save there', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const edits = {feature_custom_discriminator: 1, max_guilds: DEFAULT_STOCK_LIMITS.max_guilds};
|
||||
await saveDefaultRule(admin, edits);
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
await saveDefaultRule(admin, {max_bio_length: 300});
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const rule = defaultRule(await getLimitConfig(admin));
|
||||
expect(rule.limits).toEqual({...DEFAULT_RESTRICTED_LIMITS, ...edits, max_bio_length: 300});
|
||||
expect(rule.modifiedFields).toEqual(['feature_custom_discriminator', 'max_bio_length', 'max_guilds']);
|
||||
});
|
||||
|
||||
it('keeps premium rule edits across a save while everyone hides the rule', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const edits = {max_guilds: 999, feature_animated_avatar: 0};
|
||||
await saveRule(admin, 'premium', edits);
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
const hidden = await getLimitConfig(admin);
|
||||
expect(hidden.limit_config.rules.map((rule) => rule.id)).toEqual(['default', 'very_large_guild']);
|
||||
expect(hidden.limit_config.traitDefinitions).toEqual([]);
|
||||
await saveDefaultRule(admin, {max_bio_length: 300});
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const response = await getLimitConfig(admin);
|
||||
expect(findRule(response, 'premium').limits).toEqual({...DEFAULT_STOCK_LIMITS, ...edits});
|
||||
expect(response.limit_config.traitDefinitions).toEqual(['premium']);
|
||||
expect(defaultRule(response).limits).toEqual({...DEFAULT_RESTRICTED_LIMITS, max_bio_length: 300});
|
||||
});
|
||||
|
||||
it('lets only premium members change their tag in mirror mode', async () => {
|
||||
const admin = await createAdmin();
|
||||
await setPremiumMode(admin, 'everyone');
|
||||
await saveDefaultRule(admin, {});
|
||||
await setPremiumMode(admin, 'mirror');
|
||||
const free = await createTestAccount(harness);
|
||||
const subscriber = await createTestAccount(harness);
|
||||
const gifted = await createTestAccount(harness);
|
||||
await grantPremium(harness, subscriber.userId, UserPremiumTypes.SUBSCRIPTION);
|
||||
await grantPremium(harness, gifted.userId, UserPremiumTypes.LIFETIME);
|
||||
expect(await changeTag(free, '4242')).toBe(400);
|
||||
expect(await changeTag(subscriber, '4242')).toBe(200);
|
||||
expect(await changeTag(gifted, '4243')).toBe(200);
|
||||
const {json: me} = await fetchUserMe(harness, subscriber.token);
|
||||
expect(me.traits).toContain('premium');
|
||||
const discovery = await createBuilderWithoutAuth<WellKnownFluxerResponse>(harness)
|
||||
.get('/.well-known/fluxer')
|
||||
.execute();
|
||||
const {limits} = resolveLimits(expandWireFormat(discovery.limits), {
|
||||
traits: new Set(me.traits),
|
||||
guildFeatures: new Set(),
|
||||
});
|
||||
expect(limits.feature_custom_discriminator).toBe(1);
|
||||
expect(limits.max_guilds).toBe(DEFAULT_STOCK_LIMITS.max_guilds);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {LIMIT_KEY_METADATA, LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {MAX_GUILD_MEMBERS_VERY_LARGE_GUILD} from '@fluxer/constants/src/LimitConstants';
|
||||
import {DEFAULT_RESTRICTED_LIMITS, DEFAULT_STOCK_LIMITS} from '@fluxer/limits/src/LimitDefaults';
|
||||
import type {LimitConfigSnapshot, LimitRule} from '@fluxer/limits/src/LimitTypes';
|
||||
@@ -20,16 +20,22 @@ export function getLegacyLimitConfigKvKey(selfHosted: boolean): string {
|
||||
|
||||
export const LIMIT_CONFIG_REFRESH_CHANNEL = 'limit-config-refresh';
|
||||
|
||||
export interface LimitBuildOptions {
|
||||
selfHosted?: boolean;
|
||||
premiumMode?: 'mirror' | 'everyone';
|
||||
}
|
||||
|
||||
export interface StoredLimitConfig extends LimitConfigSnapshot {
|
||||
overridesOnly?: boolean;
|
||||
}
|
||||
|
||||
function cloneLimitConfigSnapshot(config: LimitConfigSnapshot): LimitConfigSnapshot {
|
||||
return structuredClone(config);
|
||||
}
|
||||
|
||||
export function sanitizeLimitConfigForInstance(
|
||||
config: LimitConfigSnapshot,
|
||||
options?: {
|
||||
selfHosted?: boolean;
|
||||
premiumMode?: 'mirror' | 'everyone';
|
||||
},
|
||||
options?: LimitBuildOptions,
|
||||
): LimitConfigSnapshot {
|
||||
const selfHosted = options?.selfHosted ?? false;
|
||||
const premiumMode = options?.premiumMode ?? 'everyone';
|
||||
@@ -51,10 +57,7 @@ export function sanitizeLimitConfigForInstance(
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultLimitConfig(options?: {
|
||||
selfHosted?: boolean;
|
||||
premiumMode?: 'mirror' | 'everyone';
|
||||
}): LimitConfigSnapshot {
|
||||
export function createDefaultLimitConfig(options?: LimitBuildOptions): LimitConfigSnapshot {
|
||||
const selfHosted = options?.selfHosted ?? false;
|
||||
const premiumMode = options?.premiumMode ?? 'everyone';
|
||||
const useTiers = !selfHosted || premiumMode === 'mirror';
|
||||
@@ -94,10 +97,7 @@ export function createDefaultLimitConfig(options?: {
|
||||
|
||||
export function mergeWithCurrentDefaults(
|
||||
stored: LimitConfigSnapshot,
|
||||
options?: {
|
||||
selfHosted?: boolean;
|
||||
premiumMode?: 'mirror' | 'everyone';
|
||||
},
|
||||
options?: LimitBuildOptions,
|
||||
): LimitConfigSnapshot {
|
||||
const selfHosted = options?.selfHosted ?? false;
|
||||
const premiumMode = options?.premiumMode ?? 'everyone';
|
||||
@@ -107,7 +107,7 @@ export function mergeWithCurrentDefaults(
|
||||
const mergedRules: Array<LimitRule> = [];
|
||||
const existingRulesMap = new Map<string, LimitRule>();
|
||||
for (const rule of stored.rules) {
|
||||
existingRulesMap.set(rule.id, rule);
|
||||
existingRulesMap.set(rule.id, {...rule, limits: clampToAllowedRange(rule.limits)});
|
||||
}
|
||||
for (const defaultRule of newDefaults.rules) {
|
||||
const existingRule = existingRulesMap.get(defaultRule.id);
|
||||
@@ -138,11 +138,136 @@ export function mergeWithCurrentDefaults(
|
||||
});
|
||||
}
|
||||
return {
|
||||
traitDefinitions: stored.traitDefinitions,
|
||||
traitDefinitions: [...new Set([...newDefaults.traitDefinitions, ...stored.traitDefinitions])],
|
||||
rules: mergedRules,
|
||||
};
|
||||
}
|
||||
|
||||
export function readStoredLimitOverrides(
|
||||
stored: StoredLimitConfig,
|
||||
options: Required<LimitBuildOptions>,
|
||||
): StoredLimitConfig {
|
||||
if (!options.selfHosted || stored.overridesOnly) {
|
||||
return stored;
|
||||
}
|
||||
const currentDefaults = builtInRuleLimits(options);
|
||||
const otherDefaults = builtInRuleLimits({
|
||||
selfHosted: true,
|
||||
premiumMode: options.premiumMode === 'mirror' ? 'everyone' : 'mirror',
|
||||
});
|
||||
return {
|
||||
overridesOnly: true,
|
||||
traitDefinitions: stored.traitDefinitions,
|
||||
rules: stored.rules.map((rule) => {
|
||||
const current = currentDefaults.get(rule.id);
|
||||
const other = otherDefaults.get(rule.id);
|
||||
const savedUnder = current && !(other && wasSavedUnderOtherDefaults(rule, current, other)) ? current : other;
|
||||
return {
|
||||
id: rule.id,
|
||||
filters: rule.filters,
|
||||
limits: savedUnder ? pickModifiedLimits(rule.limits, savedUnder) : rule.limits,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildStoredLimitConfig(
|
||||
submitted: LimitConfigSnapshot,
|
||||
previous: StoredLimitConfig | null,
|
||||
options: Required<LimitBuildOptions>,
|
||||
): StoredLimitConfig {
|
||||
const visible = sanitizeLimitConfigForInstance(submitted, options);
|
||||
if (!options.selfHosted) {
|
||||
return mergeWithCurrentDefaults(visible, options);
|
||||
}
|
||||
const kept = previous === null ? null : readStoredLimitOverrides(previous, options);
|
||||
const keptRules = new Map((kept?.rules ?? []).map((rule) => [rule.id, rule]));
|
||||
const defaults = builtInRuleLimits(options);
|
||||
const rules: Array<LimitRule> = visible.rules.map((rule) => {
|
||||
const ruleDefaults = defaults.get(rule.id);
|
||||
const keptLimits = keptRules.get(rule.id)?.limits ?? {};
|
||||
return {
|
||||
id: rule.id,
|
||||
filters: rule.filters,
|
||||
limits: ruleDefaults
|
||||
? pickLimits(rule.limits, (key, value) => value !== ruleDefaults[key] || value === keptLimits[key])
|
||||
: rule.limits,
|
||||
};
|
||||
});
|
||||
const hidden = kept === null ? null : hiddenFromInstance(kept, options);
|
||||
const visibleRuleIds = new Set(rules.map((rule) => rule.id));
|
||||
return {
|
||||
overridesOnly: true,
|
||||
traitDefinitions: [
|
||||
...(hidden?.traitDefinitions ?? []).filter((trait) => !visible.traitDefinitions.includes(trait)),
|
||||
...visible.traitDefinitions,
|
||||
],
|
||||
rules: [...(hidden?.rules ?? []).filter((rule) => !visibleRuleIds.has(rule.id)), ...rules],
|
||||
};
|
||||
}
|
||||
|
||||
function hiddenFromInstance(config: LimitConfigSnapshot, options: Required<LimitBuildOptions>): LimitConfigSnapshot {
|
||||
const visible = sanitizeLimitConfigForInstance(config, options);
|
||||
return {
|
||||
traitDefinitions: config.traitDefinitions.filter((trait) => !visible.traitDefinitions.includes(trait)),
|
||||
rules: config.rules.filter((rule) => !visible.rules.includes(rule)),
|
||||
};
|
||||
}
|
||||
|
||||
function builtInRuleLimits(options?: LimitBuildOptions): Map<string, Partial<Record<LimitKey, number>>> {
|
||||
return new Map(createDefaultLimitConfig(options).rules.map((rule) => [rule.id, rule.limits]));
|
||||
}
|
||||
|
||||
function wasSavedUnderOtherDefaults(
|
||||
rule: LimitRule,
|
||||
currentDefaults: Partial<Record<LimitKey, number>>,
|
||||
otherDefaults: Partial<Record<LimitKey, number>>,
|
||||
): boolean {
|
||||
const modeKeys = LIMIT_KEYS.filter((key) => currentDefaults[key] !== otherDefaults[key]);
|
||||
const edited = new Set<string>(rule.modifiedFields ?? []);
|
||||
const untouchedModeKeys = modeKeys.filter((key) => !edited.has(key));
|
||||
if (untouchedModeKeys.length > 0) {
|
||||
return untouchedModeKeys.every((key) => rule.limits[key] === otherDefaults[key]);
|
||||
}
|
||||
return (
|
||||
modeKeys.some((key) => rule.limits[key] === otherDefaults[key]) &&
|
||||
modeKeys.every((key) => rule.limits[key] !== undefined && rule.limits[key] !== currentDefaults[key])
|
||||
);
|
||||
}
|
||||
|
||||
function clampToAllowedRange(limits: Partial<Record<LimitKey, number>>): Partial<Record<LimitKey, number>> {
|
||||
const clamped = {...limits};
|
||||
for (const key of LIMIT_KEYS) {
|
||||
const value = clamped[key];
|
||||
const {min = 0, max} = LIMIT_KEY_METADATA[key];
|
||||
if (value !== undefined && max !== undefined) {
|
||||
clamped[key] = Math.min(Math.max(value, min), max);
|
||||
}
|
||||
}
|
||||
return clamped;
|
||||
}
|
||||
|
||||
function pickModifiedLimits(
|
||||
limits: Partial<Record<LimitKey, number>>,
|
||||
defaultLimits: Partial<Record<LimitKey, number>>,
|
||||
): Partial<Record<LimitKey, number>> {
|
||||
return pickLimits(limits, (key, value) => value !== defaultLimits[key]);
|
||||
}
|
||||
|
||||
function pickLimits(
|
||||
limits: Partial<Record<LimitKey, number>>,
|
||||
keep: (key: LimitKey, value: number) => boolean,
|
||||
): Partial<Record<LimitKey, number>> {
|
||||
const picked: Partial<Record<LimitKey, number>> = {};
|
||||
for (const key of LIMIT_KEYS) {
|
||||
const value = limits[key];
|
||||
if (value !== undefined && keep(key, value)) {
|
||||
picked[key] = value;
|
||||
}
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
function findModifiedLimits(
|
||||
currentLimits: Partial<Record<LimitKey, number>>,
|
||||
defaultLimits: Partial<Record<LimitKey, number>>,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import crypto from 'node:crypto';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {APIConfig, BlueskyOAuthConfig, BlueskyOAuthKeyConfig} from '@app/api/config/APIConfig';
|
||||
import type {StoredLimitConfig} from '@app/api/constants/LimitConfig';
|
||||
import {executeConditional, fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution';
|
||||
import {Db, type PreparedQuery} from '@app/api/database/CassandraTypes';
|
||||
import type {InstanceConfigurationRow} from '@app/api/database/types/InstanceConfigTypes';
|
||||
@@ -33,7 +34,7 @@ import {setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls';
|
||||
import {getDefaultProductName, setCachedProductName} from '@app/api/instance/ProductName';
|
||||
import {normalizeSsoAllowedEmailDomains} from '@app/api/instance/SsoConfigValidation';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {isLimitConfigSnapshot} from '@app/api/limits/LimitConfigValidation';
|
||||
import {isStoredLimitConfig} from '@app/api/limits/LimitConfigValidation';
|
||||
import {
|
||||
getEffectiveBillingConfig,
|
||||
isBillingActive,
|
||||
@@ -52,7 +53,6 @@ import {
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError';
|
||||
import {ServiceUnavailableError} from '@fluxer/errors/src/domains/core/ServiceUnavailableError';
|
||||
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
|
||||
import {
|
||||
InstanceConfigResponse,
|
||||
InstanceConfigUpdateRequest,
|
||||
@@ -198,7 +198,7 @@ export interface InstanceBillingAdminConfig {
|
||||
export type InstancePremiumMode = 'mirror' | 'everyone';
|
||||
|
||||
interface LimitConfigInputs {
|
||||
config: LimitConfigSnapshot | null;
|
||||
config: StoredLimitConfig | null;
|
||||
premiumMode: InstancePremiumMode;
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ const FETCH_LIMIT_CONFIG_INPUTS_QUERY = InstanceConfiguration.selectCql({
|
||||
where: InstanceConfiguration.where.in('key', 'keys'),
|
||||
});
|
||||
|
||||
function parseStoredLimitConfig(raw: string | null): LimitConfigSnapshot | null {
|
||||
function parseStoredLimitConfig(raw: string | null): StoredLimitConfig | null {
|
||||
if (raw === null) return null;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
@@ -391,7 +391,7 @@ function parseStoredLimitConfig(raw: string | null): LimitConfigSnapshot | null
|
||||
Logger.error({error}, 'Stored limit configuration is not valid JSON, falling back to default limits');
|
||||
return null;
|
||||
}
|
||||
if (!isLimitConfigSnapshot(parsed)) {
|
||||
if (!isStoredLimitConfig(parsed)) {
|
||||
Logger.error('Stored limit configuration has an invalid shape, falling back to default limits');
|
||||
return null;
|
||||
}
|
||||
@@ -1680,7 +1680,7 @@ export class InstanceConfigRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async setLimitConfig(config: LimitConfigSnapshot): Promise<void> {
|
||||
async setLimitConfig(config: StoredLimitConfig): Promise<void> {
|
||||
await this.setConfig(LIMIT_CONFIG_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import {Config} from '@app/api/Config';
|
||||
import {
|
||||
buildStoredLimitConfig,
|
||||
createDefaultLimitConfig,
|
||||
getLegacyLimitConfigKvKey,
|
||||
LIMIT_CONFIG_REFRESH_CHANNEL,
|
||||
type LimitBuildOptions,
|
||||
mergeWithCurrentDefaults,
|
||||
readStoredLimitOverrides,
|
||||
sanitizeLimitConfigForInstance,
|
||||
} from '@app/api/constants/LimitConfig';
|
||||
import {
|
||||
@@ -28,11 +31,6 @@ const MAX_PENDING_OPERATIONS = 32;
|
||||
|
||||
let globalLimitConfigService: LimitConfigService | null = null;
|
||||
|
||||
interface LimitBuildOptions {
|
||||
selfHosted: boolean;
|
||||
premiumMode: InstancePremiumMode;
|
||||
}
|
||||
|
||||
export class LimitConfigService {
|
||||
private premiumMode: InstancePremiumMode = 'everyone';
|
||||
private config: LimitConfigSnapshot = createDefaultLimitConfig({
|
||||
@@ -146,40 +144,60 @@ export class LimitConfigService {
|
||||
this.assertRefreshActive(generation);
|
||||
const {config: stored, premiumMode} = await this.repository.readLimitConfigInputs();
|
||||
this.assertRefreshActive(generation);
|
||||
const buildOptions: LimitBuildOptions = {
|
||||
const buildOptions: Required<LimitBuildOptions> = {
|
||||
selfHosted: Config.instance.selfHosted,
|
||||
premiumMode,
|
||||
};
|
||||
this.config =
|
||||
stored === null
|
||||
? createDefaultLimitConfig(buildOptions)
|
||||
: mergeWithCurrentDefaults(sanitizeLimitConfigForInstance(stored, buildOptions), buildOptions);
|
||||
: mergeWithCurrentDefaults(
|
||||
sanitizeLimitConfigForInstance(readStoredLimitOverrides(stored, buildOptions), buildOptions),
|
||||
buildOptions,
|
||||
);
|
||||
this.premiumMode = premiumMode;
|
||||
setCachedInstancePremiumMode(premiumMode);
|
||||
}
|
||||
|
||||
updateConfig(config: LimitConfigSnapshot): Promise<void> {
|
||||
return this.runOperation(async (generation) => {
|
||||
const policy = await this.repository.readStoredInstancePolicyConfig();
|
||||
const {config: previous, premiumMode} = await this.repository.readLimitConfigInputs();
|
||||
this.assertRefreshActive(generation);
|
||||
const buildOptions: LimitBuildOptions = {
|
||||
const stored = buildStoredLimitConfig(config, previous, {
|
||||
selfHosted: Config.instance.selfHosted,
|
||||
premiumMode: policy.premium_mode,
|
||||
};
|
||||
const normalized = mergeWithCurrentDefaults(sanitizeLimitConfigForInstance(config, buildOptions), buildOptions);
|
||||
await this.repository.setLimitConfig(normalized);
|
||||
premiumMode,
|
||||
});
|
||||
await this.repository.setLimitConfig(stored);
|
||||
await this.reloadConfig(generation);
|
||||
Logger.info({ruleCount: normalized.rules.length}, 'Limit config updated');
|
||||
Logger.info({ruleCount: stored.rules.length}, 'Limit config updated');
|
||||
});
|
||||
}
|
||||
|
||||
updatePolicyConfig(patch: Partial<InstancePolicyConfig>): Promise<void> {
|
||||
return this.runOperation(async (generation) => {
|
||||
if (patch.premium_mode !== undefined) {
|
||||
await this.detachStoredLimitsFromPremiumMode(generation, patch.premium_mode);
|
||||
}
|
||||
await this.repository.setInstancePolicyConfig(patch);
|
||||
await this.reloadConfig(generation);
|
||||
});
|
||||
}
|
||||
|
||||
private async detachStoredLimitsFromPremiumMode(
|
||||
generation: number,
|
||||
nextPremiumMode: InstancePremiumMode,
|
||||
): Promise<void> {
|
||||
const {config: stored, premiumMode} = await this.repository.readLimitConfigInputs();
|
||||
this.assertRefreshActive(generation);
|
||||
if (stored === null || premiumMode === nextPremiumMode) {
|
||||
return;
|
||||
}
|
||||
const overrides = readStoredLimitOverrides(stored, {selfHosted: Config.instance.selfHosted, premiumMode});
|
||||
if (overrides !== stored) {
|
||||
await this.repository.setLimitConfig(overrides);
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadConfig(generation: number): Promise<void> {
|
||||
this.assertRefreshActive(generation);
|
||||
await this.cacheService.delete(getLegacyLimitConfigKvKey(Config.instance.selfHosted));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {StoredLimitConfig} from '@app/api/constants/LimitConfig';
|
||||
import {isJsonRecord} from '@app/api/utils/JsonBoundaryUtils';
|
||||
import type {LimitConfigSnapshot, LimitRule} from '@fluxer/limits/src/LimitTypes';
|
||||
import type {LimitRule} from '@fluxer/limits/src/LimitTypes';
|
||||
|
||||
function isStringArray(value: unknown): value is Array<string> {
|
||||
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
||||
@@ -17,10 +18,11 @@ function isLimitRuleSnapshot(value: unknown): value is LimitRule {
|
||||
);
|
||||
}
|
||||
|
||||
export function isLimitConfigSnapshot(value: unknown): value is LimitConfigSnapshot {
|
||||
export function isStoredLimitConfig(value: unknown): value is StoredLimitConfig {
|
||||
return (
|
||||
isJsonRecord(value) &&
|
||||
(value.version === undefined || typeof value.version === 'number') &&
|
||||
(value.overridesOnly === undefined || typeof value.overridesOnly === 'boolean') &&
|
||||
isStringArray(value.traitDefinitions) &&
|
||||
Array.isArray(value.rules) &&
|
||||
value.rules.every(isLimitRuleSnapshot)
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
buildStoredLimitConfig,
|
||||
createDefaultLimitConfig,
|
||||
mergeWithCurrentDefaults,
|
||||
readStoredLimitOverrides,
|
||||
sanitizeLimitConfigForInstance,
|
||||
} from '@app/api/constants/LimitConfig';
|
||||
import {LIMIT_KEYS} from '@fluxer/constants/src/LimitConfigMetadata';
|
||||
import {DEFAULT_RESTRICTED_LIMITS, DEFAULT_STOCK_LIMITS} from '@fluxer/limits/src/LimitDefaults';
|
||||
import type {LimitConfigSnapshot, LimitRule} from '@fluxer/limits/src/LimitTypes';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
describe('Limit config storage', () => {
|
||||
const mirror = {selfHosted: true, premiumMode: 'mirror'} as const;
|
||||
const everyone = {selfHosted: true, premiumMode: 'everyone'} as const;
|
||||
const hosted = {selfHosted: false, premiumMode: 'everyone'} as const;
|
||||
const modeKeys = LIMIT_KEYS.filter((key) => DEFAULT_STOCK_LIMITS[key] !== DEFAULT_RESTRICTED_LIMITS[key]);
|
||||
|
||||
function defaultRuleConfig(rule: Omit<LimitRule, 'id'>): LimitConfigSnapshot {
|
||||
return {traitDefinitions: [], rules: [{id: 'default', ...rule}]};
|
||||
}
|
||||
|
||||
function fullConfig(options: {selfHosted: boolean; premiumMode: 'mirror' | 'everyone'}): LimitConfigSnapshot {
|
||||
const {traitDefinitions, rules} = createDefaultLimitConfig(options);
|
||||
return {traitDefinitions, rules};
|
||||
}
|
||||
|
||||
function withLimits(config: LimitConfigSnapshot, ruleId: string, limits: LimitRule['limits']): LimitConfigSnapshot {
|
||||
return {
|
||||
traitDefinitions: config.traitDefinitions,
|
||||
rules: config.rules.map((rule) => (rule.id === ruleId ? {...rule, limits: {...rule.limits, ...limits}} : rule)),
|
||||
};
|
||||
}
|
||||
|
||||
function load(stored: LimitConfigSnapshot, options: typeof mirror | typeof everyone): LimitConfigSnapshot {
|
||||
const overrides = readStoredLimitOverrides(structuredClone(stored), options);
|
||||
return mergeWithCurrentDefaults(sanitizeLimitConfigForInstance(overrides, options), options);
|
||||
}
|
||||
|
||||
function limitsOf(config: LimitConfigSnapshot, ruleId: string): LimitRule['limits'] | undefined {
|
||||
return config.rules.find((rule) => rule.id === ruleId)?.limits;
|
||||
}
|
||||
|
||||
test('built-in rules keep only values that differ from the defaults', () => {
|
||||
const stored = buildStoredLimitConfig(
|
||||
{
|
||||
traitDefinitions: ['premium', 'vip'],
|
||||
rules: [
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {...DEFAULT_STOCK_LIMITS, max_guilds: 250}},
|
||||
{id: 'default', limits: {...DEFAULT_RESTRICTED_LIMITS}, modifiedFields: ['max_guilds']},
|
||||
{id: 'vip', filters: {traits: ['vip']}, limits: {max_guilds: 100}},
|
||||
],
|
||||
},
|
||||
null,
|
||||
mirror,
|
||||
);
|
||||
expect(stored.rules).toEqual([
|
||||
{id: 'premium', filters: {traits: ['premium']}, limits: {max_guilds: 250}},
|
||||
{id: 'default', limits: {}},
|
||||
{id: 'vip', filters: {traits: ['vip']}, limits: {max_guilds: 100}},
|
||||
]);
|
||||
});
|
||||
|
||||
test('hosted instances keep storing whole rules', () => {
|
||||
const submitted = withLimits(fullConfig(hosted), 'default', {max_guilds: 150});
|
||||
const stored = buildStoredLimitConfig(submitted, null, hosted);
|
||||
expect(stored).toEqual(mergeWithCurrentDefaults(submitted, hosted));
|
||||
expect(limitsOf(stored, 'default')).toEqual({...DEFAULT_RESTRICTED_LIMITS, max_guilds: 150});
|
||||
expect(readStoredLimitOverrides(stored, hosted)).toBe(stored);
|
||||
});
|
||||
|
||||
test('a whole default rule saved under the other premium model keeps only its edits', () => {
|
||||
const stored = defaultRuleConfig({
|
||||
limits: {...DEFAULT_RESTRICTED_LIMITS, max_guilds: 150, max_bio_length: 300},
|
||||
modifiedFields: ['max_guilds', 'max_bio_length'],
|
||||
});
|
||||
expect(readStoredLimitOverrides(stored, everyone).rules).toEqual([
|
||||
{id: 'default', limits: {max_guilds: 150, max_bio_length: 300}},
|
||||
]);
|
||||
});
|
||||
|
||||
test('a whole default rule saved under the current premium model keeps only its edits', () => {
|
||||
const stored = defaultRuleConfig({
|
||||
limits: {...DEFAULT_RESTRICTED_LIMITS, feature_custom_discriminator: 1},
|
||||
modifiedFields: ['feature_custom_discriminator'],
|
||||
});
|
||||
expect(readStoredLimitOverrides(stored, mirror).rules).toEqual([
|
||||
{id: 'default', limits: {feature_custom_discriminator: 1}},
|
||||
]);
|
||||
});
|
||||
|
||||
test('free limits saved again after a switch to everyone are dropped', () => {
|
||||
const savedInMirror = mergeWithCurrentDefaults(fullConfig(mirror), mirror);
|
||||
const shownInEveryone = mergeWithCurrentDefaults(savedInMirror, everyone);
|
||||
expect(limitsOf(shownInEveryone, 'default')).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
const savedAgain = mergeWithCurrentDefaults(shownInEveryone, everyone);
|
||||
expect(savedAgain.rules.find((rule) => rule.id === 'default')?.modifiedFields).toEqual(modeKeys);
|
||||
expect(limitsOf(load(savedAgain, everyone), 'default')).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
expect(limitsOf(load(savedAgain, mirror), 'default')).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
});
|
||||
|
||||
test('free limits saved again with an edit after a switch to everyone keep only the edit', () => {
|
||||
const savedInMirror = mergeWithCurrentDefaults(
|
||||
withLimits(fullConfig(mirror), 'default', {max_guilds: 150, max_bio_length: 300}),
|
||||
mirror,
|
||||
);
|
||||
const savedAgain = mergeWithCurrentDefaults(mergeWithCurrentDefaults(savedInMirror, everyone), everyone);
|
||||
expect(savedAgain.rules.find((rule) => rule.id === 'default')?.modifiedFields).toEqual(
|
||||
LIMIT_KEYS.filter((key) => modeKeys.includes(key) || key === 'max_bio_length'),
|
||||
);
|
||||
expect(readStoredLimitOverrides(savedAgain, everyone).rules.find((rule) => rule.id === 'default')).toEqual({
|
||||
id: 'default',
|
||||
limits: {max_bio_length: 300, max_guilds: 150},
|
||||
});
|
||||
});
|
||||
|
||||
test('a default rule fixed by hand after a switch is left as saved', () => {
|
||||
const stored = defaultRuleConfig({
|
||||
limits: {...DEFAULT_RESTRICTED_LIMITS, feature_custom_discriminator: 1},
|
||||
modifiedFields: modeKeys.filter((key) => key !== 'feature_custom_discriminator'),
|
||||
});
|
||||
expect(limitsOf(load(stored, everyone), 'default')).toEqual({
|
||||
...DEFAULT_RESTRICTED_LIMITS,
|
||||
feature_custom_discriminator: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('premium limits saved again after a switch to mirror are dropped', () => {
|
||||
const savedInEveryone = mergeWithCurrentDefaults(fullConfig(everyone), everyone);
|
||||
const shownInMirror = mergeWithCurrentDefaults(savedInEveryone, mirror);
|
||||
expect(limitsOf(shownInMirror, 'default')).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
const savedAgain = mergeWithCurrentDefaults(shownInMirror, mirror);
|
||||
const loaded = load(savedAgain, mirror);
|
||||
expect(limitsOf(loaded, 'default')).toEqual(DEFAULT_RESTRICTED_LIMITS);
|
||||
expect(limitsOf(loaded, 'premium')).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
expect(loaded.traitDefinitions).toEqual(['premium']);
|
||||
});
|
||||
|
||||
test('overrides saved in the current format are never reinterpreted', () => {
|
||||
const raised = buildStoredLimitConfig(
|
||||
withLimits(fullConfig(mirror), 'default', DEFAULT_STOCK_LIMITS),
|
||||
null,
|
||||
mirror,
|
||||
);
|
||||
expect(Object.keys(limitsOf(raised, 'default') ?? {})).toEqual(modeKeys);
|
||||
expect(readStoredLimitOverrides(raised, mirror)).toBe(raised);
|
||||
expect(readStoredLimitOverrides(raised, everyone)).toBe(raised);
|
||||
expect(limitsOf(load(raised, mirror), 'default')).toEqual(DEFAULT_STOCK_LIMITS);
|
||||
});
|
||||
|
||||
test('an edit that matches the other premium model survives a save there', () => {
|
||||
const edits = {feature_custom_discriminator: 1, max_guilds: DEFAULT_STOCK_LIMITS.max_guilds};
|
||||
const savedInMirror = buildStoredLimitConfig(withLimits(fullConfig(mirror), 'default', edits), null, mirror);
|
||||
const savedInEveryone = buildStoredLimitConfig(
|
||||
withLimits(load(savedInMirror, everyone), 'default', {max_bio_length: 300}),
|
||||
savedInMirror,
|
||||
everyone,
|
||||
);
|
||||
expect(limitsOf(savedInEveryone, 'default')).toEqual({...edits, max_bio_length: 300});
|
||||
expect(limitsOf(load(savedInEveryone, mirror), 'default')).toEqual({
|
||||
...DEFAULT_RESTRICTED_LIMITS,
|
||||
...edits,
|
||||
max_bio_length: 300,
|
||||
});
|
||||
});
|
||||
|
||||
test('a value moved back to the default stops being stored', () => {
|
||||
const savedInMirror = buildStoredLimitConfig(
|
||||
withLimits(fullConfig(mirror), 'default', {max_guilds: 150}),
|
||||
null,
|
||||
mirror,
|
||||
);
|
||||
const reverted = buildStoredLimitConfig(fullConfig(mirror), savedInMirror, mirror);
|
||||
expect(limitsOf(reverted, 'default')).toEqual({});
|
||||
});
|
||||
|
||||
test('premium rule edits survive a save while everyone hides the rule', () => {
|
||||
const edits = {max_guilds: 999, feature_custom_discriminator: 0};
|
||||
const savedInMirror = buildStoredLimitConfig(withLimits(fullConfig(mirror), 'premium', edits), null, mirror);
|
||||
expect(limitsOf(load(savedInMirror, everyone), 'premium')).toBeUndefined();
|
||||
const savedInEveryone = buildStoredLimitConfig(fullConfig(everyone), savedInMirror, everyone);
|
||||
expect(savedInEveryone.traitDefinitions).toEqual(['premium']);
|
||||
expect(limitsOf(savedInEveryone, 'premium')).toEqual(edits);
|
||||
expect(limitsOf(load(savedInEveryone, mirror), 'premium')).toEqual({...DEFAULT_STOCK_LIMITS, ...edits});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user