mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
refactor(schema): simplify validation and OpenAPI generation (#2709)
This commit is contained in:
1 parent
4bc5593f9f
commit
de1fd95a99
510 files changed
+29494
-36131
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user