helpers for error assertions

This commit is contained in:
Kagami Sascha Rosylight 2023-06-11 20:58:28 +02:00
parent 0d2041f5aa
commit d245306d90

View file

@ -27,15 +27,6 @@ const basicAuthParams: AuthorizationParamsExtended = {
code_challenge_method: 'S256', code_challenge_method: 'S256',
}; };
interface OAuthErrorResponse {
error: string;
error_description: string;
}
interface OAuthErrorDirectResponse {
code: string;
}
interface AuthorizationParamsExtended { interface AuthorizationParamsExtended {
redirect_uri: string; redirect_uri: string;
scope: string | string[]; scope: string | string[];
@ -122,6 +113,27 @@ async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: st
return { client, code }; return { client, code };
} }
function assertIndirectError(response: Response, error: string): void {
assert.strictEqual(response.status, 302);
const location = response.headers.get('location');
assert.ok(location);
assert.strictEqual(new URL(location).searchParams.get('error'), error);
}
async function assertDirectError(response: Response, status: number, error: string): Promise<void> {
assert.strictEqual(response.status, status);
const data = await response.json();
// `mode: indirect` may throw a direct error with `code` while the default direct mode uses `error`
// For now this doesn't matter too much since direct errors are not intended to be sent to clients.
if ('code' in data) {
assert.strictEqual(data.code, error);
} else {
assert.strictEqual(data.error, error);
}
}
describe('OAuth', () => { describe('OAuth', () => {
let app: INestApplicationContext; let app: INestApplicationContext;
let fastify: FastifyInstance; let fastify: FastifyInstance;
@ -299,11 +311,7 @@ describe('OAuth', () => {
scope: 'write:notes', scope: 'write:notes',
state: 'state', state: 'state',
}), { redirect: 'manual' }); }), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_request');
let location = response.headers.get('location');
assert.ok(location);
assert.strictEqual(new URL(location).searchParams.get('error'), 'invalid_request');
// Pattern 2: Only code_challenge // Pattern 2: Only code_challenge
response = await fetch(client.authorizeURL({ response = await fetch(client.authorizeURL({
@ -312,11 +320,7 @@ describe('OAuth', () => {
state: 'state', state: 'state',
code_challenge: 'code', code_challenge: 'code',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_request');
location = response.headers.get('location');
assert.ok(location);
assert.strictEqual(new URL(location).searchParams.get('error'), 'invalid_request');
// Pattern 2: Only code_challenge_method // Pattern 2: Only code_challenge_method
response = await fetch(client.authorizeURL({ response = await fetch(client.authorizeURL({
@ -325,11 +329,7 @@ describe('OAuth', () => {
state: 'state', state: 'state',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_request');
location = response.headers.get('location');
assert.ok(location);
assert.strictEqual(new URL(location).searchParams.get('error'), 'invalid_request');
// Pattern 3: Unsupported code_challenge_method // Pattern 3: Unsupported code_challenge_method
response = await fetch(client.authorizeURL({ response = await fetch(client.authorizeURL({
@ -339,11 +339,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'SSSS', code_challenge_method: 'SSSS',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_request');
location = response.headers.get('location');
assert.ok(location);
assert.strictEqual(new URL(location).searchParams.get('error'), 'invalid_request');
}); });
// Use precomputed challenge/verifier set here for deterministic test // Use precomputed challenge/verifier set here for deterministic test
@ -441,12 +437,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_scope');
const locationHeader = response.headers.get('location');
assert.ok(locationHeader);
assert.strictEqual(new URL(locationHeader).searchParams.get('error'), 'invalid_scope');
}); });
test('Empty scope', async () => { test('Empty scope', async () => {
@ -459,12 +450,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_scope');
const locationHeader = response.headers.get('location');
assert.ok(locationHeader);
assert.strictEqual(new URL(locationHeader).searchParams.get('error'), 'invalid_scope');
}); });
test('Unknown scopes', async () => { test('Unknown scopes', async () => {
@ -477,12 +463,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended), { redirect: 'manual' }); } as AuthorizationParamsExtended), { redirect: 'manual' });
assert.strictEqual(response.status, 302); assertIndirectError(response, 'invalid_scope');
const locationHeader = response.headers.get('location');
assert.ok(locationHeader);
assert.strictEqual(new URL(locationHeader).searchParams.get('error'), 'invalid_scope');
}); });
test('Partially known scopes', async () => { test('Partially known scopes', async () => {
@ -610,9 +591,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended)); } as AuthorizationParamsExtended));
await assertDirectError(response, 400, 'invalid_request');
assert.strictEqual(response.status, 400);
assert.strictEqual((await response.json() as OAuthErrorDirectResponse).code, 'invalid_request');
}); });
test('Invalid redirect_uri including the valid one at authorization endpoint', async () => { test('Invalid redirect_uri including the valid one at authorization endpoint', async () => {
@ -625,9 +604,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended)); } as AuthorizationParamsExtended));
await assertDirectError(response, 400, 'invalid_request');
assert.strictEqual(response.status, 400);
assert.strictEqual((await response.json() as OAuthErrorDirectResponse).code, 'invalid_request');
}); });
test('No redirect_uri at authorization endpoint', async () => { test('No redirect_uri at authorization endpoint', async () => {
@ -639,9 +616,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended)); } as AuthorizationParamsExtended));
await assertDirectError(response, 400, 'invalid_request');
assert.strictEqual(response.status, 400);
assert.strictEqual((await response.json() as OAuthErrorDirectResponse).code, 'invalid_request');
}); });
test('Invalid redirect_uri at token endpoint', async () => { test('Invalid redirect_uri at token endpoint', async () => {
@ -689,6 +664,8 @@ describe('OAuth', () => {
assert.ok(body.scopes_supported.includes('write:notes')); assert.ok(body.scopes_supported.includes('write:notes'));
}); });
// Any error on decision endpoint is solely on Misskey side and nothing to do with the client.
// Do not use indirect error here.
describe('Decision endpoint', () => { describe('Decision endpoint', () => {
test('No login token', async () => { test('No login token', async () => {
const client = getClient(); const client = getClient();
@ -709,9 +686,7 @@ describe('OAuth', () => {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
}, },
}); });
await assertDirectError(decisionResponse, 400, 'invalid_request');
assert.strictEqual(decisionResponse.status, 400);
assert.strictEqual((await decisionResponse.json() as OAuthErrorResponse).error, 'invalid_request');
}); });
test('No transaction ID', async () => { test('No transaction ID', async () => {
@ -725,9 +700,7 @@ describe('OAuth', () => {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
}, },
}); });
await assertDirectError(decisionResponse, 400, 'invalid_request');
assert.strictEqual(decisionResponse.status, 400);
assert.strictEqual((await decisionResponse.json() as OAuthErrorResponse).error, 'invalid_request');
}); });
test('Invalid transaction ID', async () => { test('Invalid transaction ID', async () => {
@ -742,9 +715,7 @@ describe('OAuth', () => {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
}, },
}); });
await assertDirectError(decisionResponse, 403, 'access_denied');
assert.strictEqual(decisionResponse.status, 403);
assert.strictEqual((await decisionResponse.json() as OAuthErrorResponse).error, 'access_denied');
}); });
}); });
@ -826,8 +797,8 @@ describe('OAuth', () => {
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended)); } as AuthorizationParamsExtended));
assert.strictEqual(response.status, 400); // direct error because there's no redirect URI to ping
assert.strictEqual((await response.json() as OAuthErrorDirectResponse).code, 'invalid_request'); await assertDirectError(response, 400, 'invalid_request');
}); });
}); });
@ -842,9 +813,7 @@ describe('OAuth', () => {
code_challenge: 'code', code_challenge: 'code',
code_challenge_method: 'S256', code_challenge_method: 'S256',
} as AuthorizationParamsExtended)); } as AuthorizationParamsExtended));
await assertDirectError(response, 400, 'invalid_request');
assert.strictEqual(response.status, 400);
assert.strictEqual((await response.json() as OAuthErrorDirectResponse).code, 'invalid_request');
}); });
test('Missing name', async () => { test('Missing name', async () => {