refactor(schema): simplify validation and OpenAPI generation (#2709)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-11 23:24:26 +02:00
1 parent 4bc5593f9f
commit de1fd95a99
510 files changed
+29494 -36131

No files matched your search

@@ -61,12 +61,9 @@ describe('ChannelResponse', () => {
rate_limit_per_user: 0,
};
it('accepts valid channel response', () => {
const result = ChannelResponse.safeParse(validChannel);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('123456789012345678');
expect(result.data.name).toBe('general');
}
const result = ChannelResponse.parse(validChannel);
expect(result.id).toBe('123456789012345678');
expect(result.name).toBe('general');
});
it('requires id and type', () => {
const {id, ...channelWithoutId} = validChannel;
@@ -100,11 +97,8 @@ describe('ChannelResponse', () => {
{id: '222222222222222222', type: 1, allow: '0', deny: '2048'},
],
};
const result = ChannelResponse.safeParse(channel);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.permission_overwrites).toHaveLength(2);
}
const result = ChannelResponse.parse(channel);
expect(result.permission_overwrites).toHaveLength(2);
});
it('accepts more permission_overwrites than the old response ceiling', () => {
const channel = {
@@ -116,11 +110,8 @@ describe('ChannelResponse', () => {
deny: '0',
})),
};
const result = ChannelResponse.safeParse(channel);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.permission_overwrites).toHaveLength(501);
}
const result = ChannelResponse.parse(channel);
expect(result.permission_overwrites).toHaveLength(501);
});
it('accepts voice channel properties', () => {
const voiceChannel = {
@@ -131,13 +122,10 @@ describe('ChannelResponse', () => {
voice_connection_limit: 5,
rtc_region: 'us-west',
};
const result = ChannelResponse.safeParse(voiceChannel);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.bitrate).toBe(64000);
expect(result.data.user_limit).toBe(10);
expect(result.data.voice_connection_limit).toBe(5);
}
const result = ChannelResponse.parse(voiceChannel);
expect(result.bitrate).toBe(64000);
expect(result.user_limit).toBe(10);
expect(result.voice_connection_limit).toBe(5);
});
it('accepts null optional fields', () => {
const channel = {
@@ -159,11 +147,8 @@ describe('ChannelResponse', () => {
'222222222222222222': 'Friend 2',
},
};
const result = ChannelResponse.safeParse(channel);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.nicks).toBeDefined();
}
const result = ChannelResponse.parse(channel);
expect(result.nicks).toBeDefined();
});
it('accepts valid last_pin_timestamp', () => {
const channel = {
@@ -0,0 +1,37 @@
import {
DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
ExperimentAssignmentsResponse,
ExperimentDeliveryConfigSchema,
ExperimentDeliveryConfigUpdateRequest,
} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
import {describe, expect, it} from 'vitest';
describe('experiment delivery schemas', () => {
it('derives complete configuration defaults without filling partial updates', () => {
expect(ExperimentDeliveryConfigSchema.parse({})).toEqual(DEFAULT_EXPERIMENT_DELIVERY_CONFIG);
expect(ExperimentDeliveryConfigUpdateRequest.parse({})).toEqual({});
expect(ExperimentDeliveryConfigUpdateRequest.parse({poll_interval_seconds: 3600})).toEqual({
poll_interval_seconds: 3600,
});
});
it.each([
{poll_interval_seconds: 59},
{poll_interval_seconds: 86401},
{poll_jitter_percent: -1},
{poll_jitter_percent: 51},
])('uses the same delivery bounds in configuration and updates: %j', (value) => {
expect(ExperimentDeliveryConfigSchema.safeParse(value).success).toBe(false);
expect(ExperimentDeliveryConfigUpdateRequest.safeParse(value).success).toBe(false);
});
it.each([1, 999999])('preserves server poll intervals for client-side clamping: %i', (interval) => {
expect(
ExperimentAssignmentsResponse.parse({
...DEFAULT_EXPERIMENT_DELIVERY_CONFIG,
poll_interval_seconds: interval,
assignments: {},
}).poll_interval_seconds,
).toBe(interval);
});
});
@@ -6,13 +6,34 @@ import {
MAX_TEMP_BAN_DURATION_SECONDS,
MIN_TEMP_BAN_DURATION_SECONDS,
} from '@fluxer/constants/src/LimitConstants';
import {
DiscoveryApplicationPatchRequest,
DiscoveryApplicationRequest,
DiscoverySearchQuery,
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
import {
GuildBanCreateRequest,
GuildMemberUpdateRequest,
GuildStickerCreateRequest,
} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
import {TemplateChannel} from '@fluxer/schema/src/domains/guild/GuildTemplateSchemas';
import {describe, expect, it} from 'vitest';
describe.each([
{name: 'application', schema: DiscoveryApplicationRequest.shape.primary_language},
{name: 'application patch', schema: DiscoveryApplicationPatchRequest.shape.primary_language},
{name: 'search', schema: DiscoverySearchQuery.shape.language},
])('discovery $name language', ({schema}) => {
it.each(['en-US', 'sv-SE', undefined])('preserves supported or omitted language %j', (language) => {
expect(schema.parse(language)).toBe(language);
});
it.each(['unsupported', 'EN-US', ' en-US '])('rejects unsupported language %j without normalization', (language) => {
expect(schema.safeParse(language).error?.issues).toEqual([
{code: 'custom', message: 'Unsupported language code', path: []},
]);
});
});
describe('GuildBanCreateRequest', () => {
it('accepts permanent bans and arbitrary temporary durations within range', () => {
expect(GuildBanCreateRequest.safeParse({ban_duration_seconds: 0}).success).toBe(true);
@@ -57,3 +78,38 @@ describe('GuildStickerCreateRequest', () => {
).toBe(false);
});
});
describe('TemplateChannel permission overwrites', () => {
it.each([
['role', 0],
['member', 1],
['0', 0],
[1, 1],
[42, 42],
])('normalizes the imported overwrite type %j', (type, expected) => {
const channel = TemplateChannel.parse({
id: '1',
type: 0,
position: 0,
permission_overwrites: [{id: '2', type, allow: '8', deny: '0'}],
});
expect(channel.permission_overwrites).toEqual([{id: '2', type: expected, allow: '8', deny: '0'}]);
});
it.each([
'invalid',
'NaN',
'Infinity',
Number.NaN,
Number.POSITIVE_INFINITY,
])('rejects nonfinite overwrite type %j', (type) => {
expect(
TemplateChannel.safeParse({
id: '1',
type: 0,
position: 0,
permission_overwrites: [{id: '2', type, allow: '8', deny: '0'}],
}).success,
).toBe(false);
});
});
@@ -44,12 +44,9 @@ describe('GuildResponse', () => {
permissions: '8',
};
it('accepts valid guild response', () => {
const result = GuildResponse.safeParse(validGuild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('123456789012345678');
expect(result.data.name).toBe('Test Guild');
}
const result = GuildResponse.parse(validGuild);
expect(result.id).toBe('123456789012345678');
expect(result.name).toBe('Test Guild');
});
it('accepts guild with null optional fields', () => {
const guild = {
@@ -116,33 +113,24 @@ describe('GuildResponse', () => {
...validGuild,
features: [GuildFeatures.RAID_DETECTED, GuildFeatures.VERIFIED],
};
const result = GuildResponse.safeParse(guild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.features).toEqual([GuildFeatures.RAID_DETECTED, GuildFeatures.VERIFIED]);
}
const result = GuildResponse.parse(guild);
expect(result.features).toEqual([GuildFeatures.RAID_DETECTED, GuildFeatures.VERIFIED]);
});
it('preserves unknown features in guild response', () => {
const guild = {
...validGuild,
features: [GuildFeatures.VERIFIED, 'DISALLOW_UNCLAIMED_ACCOUNTS'],
};
const result = GuildResponse.safeParse(guild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
}
const result = GuildResponse.parse(guild);
expect(result.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
});
it('deduplicates and sorts guild features while preserving values', () => {
const guild = {
...validGuild,
features: [GuildFeatures.VERIFIED, 'DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED],
};
const result = GuildResponse.safeParse(guild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
}
const result = GuildResponse.parse(guild);
expect(result.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
});
it('rejects non-integer system_channel_flags', () => {
const guild = {...validGuild, system_channel_flags: 1.5};
@@ -174,12 +162,9 @@ describe('GuildPartialResponse', () => {
features: [GuildFeatures.VERIFIED],
};
it('accepts valid partial guild response', () => {
const result = GuildPartialResponse.safeParse(validPartialGuild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('123456789012345678');
expect(result.data.name).toBe('Test Guild');
}
const result = GuildPartialResponse.parse(validPartialGuild);
expect(result.id).toBe('123456789012345678');
expect(result.name).toBe('Test Guild');
});
it('accepts partial guild with null optional fields', () => {
const guild = {
@@ -212,11 +197,8 @@ describe('GuildPartialResponse', () => {
...validPartialGuild,
features: [GuildFeatures.VERIFIED, 'DISALLOW_UNCLAIMED_ACCOUNTS'],
};
const result = GuildPartialResponse.safeParse(guild);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
}
const result = GuildPartialResponse.parse(guild);
expect(result.features).toEqual(['DISALLOW_UNCLAIMED_ACCOUNTS', GuildFeatures.VERIFIED]);
});
});
@@ -10,10 +10,47 @@ describe('jobs schemas', () => {
expect(JobLedgerEntrySchema.shape.status.safeParse('failed').success).toBe(false);
});
test('the job status enum accepts every status the ledger writes', () => {
for (const status of ['queued', 'running', 'succeeded', 'cancelled', 'deadletter']) {
expect(ListJobsQuery.safeParse({status}).success).toBe(true);
expect(JobLedgerEntrySchema.shape.status.safeParse(status).success).toBe(true);
}
test.each([
'queued',
'running',
'succeeded',
'cancelled',
'deadletter',
])('accepts ledger status %s across query, request, and response', (status) => {
expect(ListJobsQuery.parse({status}).status).toBe(status);
expect(ListJobsRequest.parse({status}).status).toBe(status);
expect(JobLedgerEntrySchema.shape.status.parse(status)).toBe(status);
});
});
describe('job pagination cursor', () => {
const cursor = {
cursor_bucket_day: '2024-02-29',
cursor_created_at: '2024-02-29T12:00:00Z',
cursor_job_id: '123',
};
test('accepts a complete cursor and applies query defaults', () => {
expect(ListJobsQuery.parse(cursor)).toEqual({...cursor, limit: 50, max_lookback_days: 14});
});
test.each([
'cursor_bucket_day',
'cursor_created_at',
'cursor_job_id',
] as const)('rejects a cursor missing %s', (field) => {
expect(ListJobsQuery.safeParse({...cursor, [field]: undefined})).toMatchObject({
success: false,
error: {issues: [{path: [field]}]},
});
});
test.each([
{cursor_bucket_day: '2023-02-29'},
{cursor_bucket_day: '2024-04-31'},
{cursor_created_at: '2024-02-30T12:00:00Z'},
{cursor_job_id: '01'},
])('rejects malformed cursor component %j', (invalid) => {
expect(ListJobsQuery.safeParse({...cursor, ...invalid}).success).toBe(false);
});
});
@@ -1,12 +1,35 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
MessageNonceRequest,
MessageRequestSchema,
MessageUpdateRequestSchema,
RichEmbedRequest,
} from '@fluxer/schema/src/domains/message/MessageRequestSchemas';
import {describe, expect, it} from 'vitest';
describe('MessageNonceRequest', () => {
it.each([
[0, '0'],
[Number.MAX_SAFE_INTEGER, '9007199254740991'],
[' client-nonce ', 'client-nonce'],
['-1', '-1'],
])('normalizes nonce %j to its wire string', (input, expected) => {
expect(MessageNonceRequest.parse(input)).toBe(expected);
});
it.each([
-1,
0.5,
Number.MAX_SAFE_INTEGER + 1,
Number.POSITIVE_INFINITY,
'',
'a'.repeat(33),
])('rejects invalid nonce %j', (input) => {
expect(MessageNonceRequest.safeParse(input).success).toBe(false);
});
});
describe('MessageRequestSchema', () => {
it('preserves ANSI escape characters in message content', () => {
const content = '```ansi\nWelcome to \u001b[2;33mFluxer\u001b[0m!\n```';
@@ -1,7 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {MAX_APPLICATION_REDIRECT_URIS} from '@fluxer/constants/src/LimitConstants';
import {ApplicationCreateRequest, ApplicationResponse} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
import {UserAuthenticatorTypes} from '@fluxer/constants/src/UserConstants';
import {
ApplicationCreateRequest,
ApplicationResponse,
ApplicationUpdateRequest,
AuthorizeConsentRequest,
AuthorizeRequest,
IntrospectRequestForm,
RevokeRequestForm,
} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
import {describe, expect, it} from 'vitest';
const buildRedirectURIs = (count: number) =>
@@ -27,3 +36,139 @@ describe('application redirect_uris bounds', () => {
expect(ApplicationResponse.safeParse(buildApplicationResponse(uris)).success).toBe(false);
});
});
describe.each([
{name: 'authorize query', schema: AuthorizeRequest},
{name: 'consent request', schema: AuthorizeConsentRequest},
])('$name', ({schema}) => {
const required = {client_id: '123', scope: 'identify'};
it('normalizes shared fields without adding optional defaults', () => {
expect(schema.parse({client_id: ' 123 ', scope: ' identify '})).toEqual({client_id: 123n, scope: 'identify'});
});
it.each(['S256', 'plain'])('preserves the PKCE method %s and shared fields', (method) => {
expect(
schema.parse({
...required,
redirect_uri: ' https://example.com/callback ',
state: ' nonce ',
permissions: '0',
guild_id: '456',
channel_id: '789',
code_challenge: ' challenge ',
code_challenge_method: method,
}),
).toEqual({
client_id: 123n,
scope: 'identify',
redirect_uri: 'https://example.com/callback',
state: 'nonce',
permissions: '0',
guild_id: 456n,
channel_id: 789n,
code_challenge: 'challenge',
code_challenge_method: method,
});
});
it.each([
{client_id: undefined},
{client_id: '1.5'},
{scope: undefined},
{scope: ' '},
{state: ''},
{redirect_uri: '/relative'},
{code_challenge: ''},
{code_challenge_method: 's256'},
])('rejects malformed shared fields %j', (fields) => {
expect(schema.safeParse({...required, ...fields}).success).toBe(false);
});
it('leaves registered redirect policy to the authorization service', () => {
expect(schema.parse({...required, redirect_uri: 'http://example.com/callback'}).redirect_uri).toBe(
'http://example.com/callback',
);
});
});
describe('authorization endpoint differences', () => {
it('keeps response_type validation strict only for the authorize query', () => {
const input = {client_id: '123', scope: 'identify', response_type: 'token'};
expect(AuthorizeRequest.safeParse(input).success).toBe(false);
expect(AuthorizeConsentRequest.parse(input).response_type).toBe('token');
});
it('does not leak query-only controls into the consent request', () => {
const input = {client_id: '123', scope: 'identify', prompt: 'none', disable_guild_select: 'true'};
expect(AuthorizeRequest.parse(input)).toEqual({...input, client_id: 123n});
expect(AuthorizeConsentRequest.parse(input)).toEqual({client_id: 123n, scope: 'identify'});
});
});
describe.each([
{name: 'introspection', schema: IntrospectRequestForm},
{name: 'revocation', schema: RevokeRequestForm},
])('$name credentials', ({schema}) => {
it('keeps body credentials optional for authorization-header authentication', () => {
expect(schema.parse({token: ' token '})).toEqual({token: 'token'});
});
it('normalizes explicit client credentials', () => {
expect(schema.parse({token: 'token', client_id: '123', client_secret: ' secret '})).toEqual({
token: 'token',
client_id: 123n,
client_secret: 'secret',
});
});
it.each([{token: ''}, {client_id: 'not-an-id'}, {client_secret: ''}])('rejects invalid credentials %j', (fields) => {
expect(schema.safeParse({token: 'token', ...fields}).success).toBe(false);
});
});
it.each(['access_token', 'refresh_token'])('preserves %s hint only on revocation', (hint) => {
const input = {token: 'token', token_type_hint: hint};
expect(RevokeRequestForm.parse(input)).toEqual(input);
expect(IntrospectRequestForm.parse(input)).toEqual({token: 'token'});
});
describe('application redirect policies', () => {
it.each([
'https://example.com/callback',
'http://localhost/callback',
'http://app.localhost/callback',
'http://127.0.0.1:3000/callback',
'http://[::1]:3000/callback',
'http://192.0.2.1/callback',
])('accepts registered URI %s', (uri) => {
expect(ApplicationCreateRequest.parse({name: 'Application', redirect_uris: [uri]}).redirect_uris).toEqual([uri]);
});
it.each([
'http://example.com/callback',
'https://',
'/relative',
'ftp://example.com/callback',
])('rejects registered URI %s', (uri) => {
expect(ApplicationCreateRequest.safeParse({name: 'Application', redirect_uris: [uri]}).success).toBe(false);
});
it('defaults missing create URIs but does not clear missing update URIs', () => {
expect(ApplicationCreateRequest.parse({name: 'Application'})).toEqual({name: 'Application', redirect_uris: []});
expect(ApplicationUpdateRequest.parse({})).toEqual({});
});
it.each([null, []])('clears explicitly supplied URIs %j', (uris) => {
expect(ApplicationUpdateRequest.parse({redirect_uris: uris})).toEqual({redirect_uris: []});
});
});
describe('application bot authenticators', () => {
const response = {
...buildApplicationResponse([]),
bot: {id: '123', username: 'bot', discriminator: '0001', bio: null, flags: 0},
};
it('accepts every canonical authenticator type', () => {
const types = Object.values(UserAuthenticatorTypes);
expect(ApplicationResponse.parse({...response, bot: {...response.bot, authenticator_types: types}}).bot).toEqual({
...response.bot,
authenticator_types: types,
});
});
it.each([-1, 0.5, 999])('rejects unknown authenticator %j', (type) => {
expect(
ApplicationResponse.safeParse({...response, bot: {...response.bot, authenticator_types: [type]}}).success,
).toBe(false);
});
});
@@ -0,0 +1,233 @@
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {
CreateVoiceServerRequest,
CreateVoiceServerRequestBody,
UpdateVoiceRegionRequest,
UpdateVoiceRegionRequestBody,
UpdateVoiceServerRequest,
UpdateVoiceServerRequestBody,
} from '@fluxer/schema/src/domains/admin/AdminVoiceSchemas';
import {ChannelUpdateRequest, ChannelUpdateRequestBody} from '@fluxer/schema/src/domains/channel/ChannelRequestSchemas';
import {
BulkDeleteSelfMessagesFilter,
BulkDeleteSelfMessagesRequest,
HarvestSelfDataRequest,
} from '@fluxer/schema/src/domains/user/UserRequestSchemas';
import {describe, expect, it} from 'vitest';
const server = {
server_id: 'primary',
endpoint: 'wss://voice.example.com',
api_key: 'key',
api_secret: 'secret',
};
describe('voice request bodies', () => {
it('requires server creation fields but not the URL-supplied region', () => {
const body = CreateVoiceServerRequestBody.parse(server);
expect(body).toEqual({
...server,
is_active: true,
soft_connection_limit: null,
vip_only: false,
required_guild_features: [],
allowed_guild_ids: [],
allowed_user_ids: [],
});
expect(CreateVoiceServerRequestBody.safeParse({}).success).toBe(false);
expect(CreateVoiceServerRequest.safeParse(server).success).toBe(false);
expect(CreateVoiceServerRequest.parse({...server, region_id: 'eu'})).toEqual({...body, region_id: 'eu'});
});
it.each(['server_id', 'endpoint', 'api_key', 'api_secret'] as const)('requires creation field %s', (field) => {
expect(CreateVoiceServerRequestBody.safeParse({...server, [field]: undefined}).success).toBe(false);
});
it('keeps patch bodies empty without injecting defaults', () => {
expect(UpdateVoiceRegionRequestBody.parse({})).toEqual({});
expect(UpdateVoiceServerRequestBody.parse({})).toEqual({});
expect(UpdateVoiceRegionRequest.parse({id: 'eu'})).toEqual({id: 'eu'});
expect(UpdateVoiceServerRequest.parse({region_id: 'eu', server_id: 'primary'})).toEqual({
region_id: 'eu',
server_id: 'primary',
});
});
it('does not treat client-supplied path fields as body properties', () => {
expect(UpdateVoiceRegionRequestBody.parse({id: 'ignored', name: 'Europe'})).toEqual({name: 'Europe'});
expect(UpdateVoiceServerRequestBody.parse({region_id: 'ignored', server_id: 'ignored'})).toEqual({});
});
it('retains explicit false, null, and snowflake transformations', () => {
expect(
UpdateVoiceServerRequestBody.parse({is_active: false, soft_connection_limit: null, allowed_user_ids: ['123']}),
).toEqual({is_active: false, soft_connection_limit: null, allowed_user_ids: [123n]});
});
});
describe.each([
{name: 'create body', schema: CreateVoiceServerRequestBody, required: server},
{name: 'create service request', schema: CreateVoiceServerRequest, required: {...server, region_id: 'eu'}},
{name: 'update body', schema: UpdateVoiceServerRequestBody, required: {}},
{name: 'update service request', schema: UpdateVoiceServerRequest, required: {region_id: 'eu', server_id: 'primary'}},
])('$name coordinates', ({schema, required}) => {
it.each([
{},
{latitude: 0, longitude: 0},
{latitude: null, longitude: null},
])('accepts the paired state %j', (coordinates) => {
expect(schema.parse({...required, ...coordinates})).toMatchObject(coordinates);
});
it.each([
{latitude: 0},
{longitude: 0},
{latitude: null},
{longitude: null},
{latitude: 0, longitude: null},
{latitude: null, longitude: 0},
])('rejects the unpaired state %j', (coordinates) => {
expect(schema.safeParse({...required, ...coordinates})).toMatchObject({
success: false,
error: {
issues: [{path: ['latitude'], message: 'Latitude and longitude must both be provided or both be omitted'}],
},
});
});
});
describe('channel update request body', () => {
it('permits an empty body while the service validator still requires the stored channel type', () => {
expect(ChannelUpdateRequestBody.parse({})).toEqual({});
expect(ChannelUpdateRequest.safeParse({}).success).toBe(false);
});
it.each([
ChannelTypes.GUILD_TEXT,
ChannelTypes.GUILD_VOICE,
ChannelTypes.GUILD_CATEGORY,
ChannelTypes.GUILD_LINK,
ChannelTypes.GROUP_DM,
])('preserves field transformations for stored channel type %i', (type) => {
const input = {name: ' General ', owner_id: '123'};
const expected = {name: 'General', owner_id: 123n};
expect(ChannelUpdateRequestBody.parse(input)).toEqual(expected);
expect(ChannelUpdateRequest.parse({...input, type})).toEqual({...expected, type});
});
it('preserves guild-only fields in the documented body', () => {
const input = {topic: ' Topic ', parent_id: '123', nsfw_override: false, bitrate: 64000, rtc_region: null};
expect(ChannelUpdateRequestBody.parse(input)).toEqual({...input, topic: 'Topic', parent_id: 123n});
});
it('does not accept the injected discriminator as a body property', () => {
expect(ChannelUpdateRequestBody.parse({type: ChannelTypes.GUILD_VOICE})).toEqual({});
});
});
const bulkDeleteDefaults = {
scope: 'selected',
include_dms: true,
include_dms_closed: true,
include_group_dms: true,
include_guilds: true,
guild_filter_mode: 'exclude',
excluded_guild_ids: [],
included_guild_ids: [],
};
const noSelectedContexts = {
include_dms: false,
include_dms_closed: false,
include_group_dms: false,
include_guilds: false,
};
const earlierDate = '2026-01-01T00:00:00Z';
const laterDate = '2026-02-01T00:00:00Z';
const selectedContextIssue = {
code: 'custom',
message: 'Enable at least one of include_dms, include_dms_closed, include_group_dms, or include_guilds.',
path: ['include_dms'],
};
const dateRangeIssue = {
code: 'custom',
message: 'start_date must be earlier than end_date.',
path: ['end_date'],
};
describe.each([
{name: 'bulk-delete filter', schema: BulkDeleteSelfMessagesFilter},
{name: 'bulk-delete request', schema: BulkDeleteSelfMessagesRequest},
{name: 'harvest request', schema: HarvestSelfDataRequest},
])('$name', ({schema}) => {
it('applies the shared defaults to an empty request', () => {
expect(schema.parse({})).toEqual(bulkDeleteDefaults);
});
it.each([
'include_dms',
'include_dms_closed',
'include_group_dms',
'include_guilds',
] as const)('accepts %s as the only selected context', (field) => {
const selection = {...noSelectedContexts, [field]: true};
expect(schema.parse(selection)).toEqual({...bulkDeleteDefaults, ...selection});
});
it('does not require selected contexts for inaccessible-only scope', () => {
const selection = {...noSelectedContexts, scope: 'inaccessible_only'};
expect(schema.parse(selection)).toEqual({...bulkDeleteDefaults, ...selection});
});
it('preserves the guild filter and transforms both guild ID lists', () => {
expect(
schema.parse({guild_filter_mode: 'include_only', included_guild_ids: ['123'], excluded_guild_ids: ['456']}),
).toEqual({
...bulkDeleteDefaults,
guild_filter_mode: 'include_only',
included_guild_ids: [123n],
excluded_guild_ids: [456n],
});
});
it.each([
{start_date: null, end_date: null},
{start_date: earlierDate},
{end_date: laterDate},
{start_date: earlierDate, end_date: null},
{start_date: null, end_date: laterDate},
{start_date: earlierDate, end_date: laterDate},
])('accepts the date bounds %j', (dates) => {
expect(schema.parse(dates)).toEqual({...bulkDeleteDefaults, ...dates});
});
it.each([
{input: noSelectedContexts, issues: [selectedContextIssue]},
{input: {start_date: earlierDate, end_date: earlierDate}, issues: [dateRangeIssue]},
{input: {start_date: laterDate, end_date: earlierDate}, issues: [dateRangeIssue]},
{
input: {...noSelectedContexts, start_date: laterDate, end_date: earlierDate},
issues: [selectedContextIssue, dateRangeIssue],
},
])('reports exact refinement issues in order for $input', ({input, issues}) => {
expect(schema.safeParse(input).error?.issues).toEqual(issues);
});
});
describe('bulk-delete sudo verification', () => {
it.each([
{password: 'correct horse battery staple'},
{mfa_method: 'totp', mfa_code: '123456'},
{mfa_method: 'webauthn', webauthn_challenge: 'challenge'},
])('preserves sudo fields only on the delete request: %j', (verification) => {
expect(BulkDeleteSelfMessagesRequest.parse(verification)).toEqual({...bulkDeleteDefaults, ...verification});
expect(BulkDeleteSelfMessagesFilter.parse(verification)).toEqual(bulkDeleteDefaults);
expect(HarvestSelfDataRequest.parse(verification)).toEqual(bulkDeleteDefaults);
});
it('still validates sudo fields on the extended request', () => {
expect(BulkDeleteSelfMessagesRequest.safeParse({mfa_method: 'unknown'})).toMatchObject({
success: false,
error: {issues: [{code: 'invalid_union', path: ['mfa_method']}]},
});
});
});
@@ -17,26 +17,21 @@ import {describe, expect, it} from 'vitest';
describe('CustomStatusPayload', () => {
it('accepts single-codepoint unicode emoji names', () => {
const result = CustomStatusPayload.safeParse({
text: 'Coffee time',
emoji_name: '☕',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.emoji_name).toBe('☕');
}
expect(
CustomStatusPayload.parse({
text: 'Coffee time',
emoji_name: '☕',
}),
).toEqual({text: 'Coffee time', emoji_name: '☕'});
});
it('ignores emoji_name when emoji_id is provided', () => {
const result = CustomStatusPayload.safeParse({
text: 'Custom emoji status',
emoji_id: '123456789012345678',
emoji_name: 'not-a-unicode-emoji-and-way-too-long-to-validate-in-this-path',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.emoji_id).toBe(123456789012345678n);
expect(result.data.emoji_name).toBeUndefined();
}
expect(
CustomStatusPayload.parse({
text: 'Custom emoji status',
emoji_id: '123456789012345678',
emoji_name: 'not-a-unicode-emoji-and-way-too-long-to-validate-in-this-path',
}),
).toEqual({text: 'Custom emoji status', emoji_id: 123456789012345678n});
});
});
@@ -47,30 +42,21 @@ describe('UserSettingsUpdateRequest synced_preferences', () => {
searchEngines: create(SearchEngineSettingsSchema, {textSearchEngineId: 'google'}),
}),
);
const result = UserSettingsUpdateRequest.safeParse({synced_preferences: encoded});
expect(result.success).toBe(true);
expect(UserSettingsUpdateRequest.parse({synced_preferences: encoded})).toEqual({synced_preferences: encoded});
});
it('accepts the empty snapshot', () => {
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: ''}).success).toBe(true);
it.each([
{name: 'empty snapshot', value: ''},
{name: 'cleared snapshot', value: null},
{name: 'exact size cap', value: 'A'.repeat(SYNCED_PREFERENCES_MAX_ENCODED_LENGTH)},
])('preserves $name', ({value}) => {
expect(UserSettingsUpdateRequest.parse({synced_preferences: value})).toEqual({synced_preferences: value});
});
it('accepts null to clear the snapshot', () => {
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: null}).success).toBe(true);
});
it('rejects non-base64 strings', () => {
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: 'not_base64!!!'}).success).toBe(false);
});
it('rejects raw record/object payloads', () => {
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: {textSearchEngineId: 'google'}}).success).toBe(
false,
);
});
it('accepts a string at exactly the size cap', () => {
const atCap = 'A'.repeat(SYNCED_PREFERENCES_MAX_ENCODED_LENGTH);
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: atCap}).success).toBe(true);
});
it('rejects strings exceeding the size cap', () => {
const oversized = 'A'.repeat(SYNCED_PREFERENCES_MAX_ENCODED_LENGTH + 1);
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: oversized}).success).toBe(false);
it.each([
{name: 'non-base64 string', value: 'not_base64!!!'},
{name: 'raw object', value: {textSearchEngineId: 'google'}},
{name: 'above size cap', value: 'A'.repeat(SYNCED_PREFERENCES_MAX_ENCODED_LENGTH + 1)},
])('rejects $name', ({value}) => {
expect(UserSettingsUpdateRequest.safeParse({synced_preferences: value}).success).toBe(false);
});
});
@@ -79,7 +65,7 @@ describe('CreatePrivateChannelRequest', () => {
const recipients = Array.from({length: MAX_GROUP_DM_OTHER_RECIPIENTS}, (_, index) =>
String(100000000000000000n + BigInt(index)),
);
expect(CreatePrivateChannelRequest.safeParse({recipients}).success).toBe(true);
expect(CreatePrivateChannelRequest.parse({recipients})).toEqual({recipients: recipients.map(BigInt)});
});
it('rejects group DM requests above the member limit', () => {
const recipients = Array.from({length: MAX_GROUP_DM_OTHER_RECIPIENTS + 1}, (_, index) =>
@@ -88,10 +74,12 @@ describe('CreatePrivateChannelRequest', () => {
expect(CreatePrivateChannelRequest.safeParse({recipients}).success).toBe(false);
});
it('allows DM requests to the system user id 0', () => {
const parsed = CreatePrivateChannelRequest.safeParse({recipient_id: '0'});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.recipient_id).toBe(0n);
}
expect(CreatePrivateChannelRequest.parse({recipient_id: '0'})).toEqual({recipient_id: 0n});
});
it.each([{}, {recipient_id: '0', recipients: []}])('requires exactly one recipient form for %j', (input) => {
expect(CreatePrivateChannelRequest.safeParse(input)).toMatchObject({
success: false,
error: {issues: [{message: 'Either recipient_id or recipients must be provided, but not both'}]},
});
});
});
@@ -19,12 +19,9 @@ describe('UserPartialResponse', () => {
flags: 0,
};
it('accepts valid user partial response', () => {
const result = UserPartialResponse.safeParse(validUser);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('123456789012345678');
expect(result.data.username).toBe('testuser');
}
const result = UserPartialResponse.parse(validUser);
expect(result.id).toBe('123456789012345678');
expect(result.username).toBe('testuser');
});
it('accepts null global_name and avatar', () => {
const user = {
@@ -38,19 +35,13 @@ describe('UserPartialResponse', () => {
});
it('accepts optional bot flag', () => {
const user = {...validUser, bot: true};
const result = UserPartialResponse.safeParse(user);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.bot).toBe(true);
}
const result = UserPartialResponse.parse(user);
expect(result.bot).toBe(true);
});
it('accepts optional system flag', () => {
const user = {...validUser, system: true};
const result = UserPartialResponse.safeParse(user);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.system).toBe(true);
}
const result = UserPartialResponse.parse(user);
expect(result.system).toBe(true);
});
it('requires id', () => {
const {id, ...userWithoutId} = validUser;
@@ -180,12 +171,9 @@ describe('RelationshipResponse', () => {
friend_shares_voice_activity: true,
};
it('accepts valid relationship', () => {
const result = RelationshipResponse.safeParse(validRelationship);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.id).toBe('123456789012345678');
expect(result.data.type).toBe(1);
}
const result = RelationshipResponse.parse(validRelationship);
expect(result.id).toBe('123456789012345678');
expect(result.type).toBe(1);
});
it('accepts relationship with null nickname', () => {
const relationship = {...validRelationship, nickname: null};