mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(core): improve error handling (#4198)
* refactor(core): improve error handling * test(core): add integration tests
This commit is contained in:
parent
bc2feb6c74
commit
6d1ea26cdc
19 changed files with 101 additions and 33 deletions
|
@ -1,27 +1,6 @@
|
|||
/**
|
||||
* Slonik Error Types:
|
||||
*
|
||||
* BackendTerminatedError,
|
||||
* CheckIntegrityConstraintViolationError,
|
||||
* ConnectionError,
|
||||
* DataIntegrityError,
|
||||
* ForeignKeyIntegrityConstraintViolationError,
|
||||
* IntegrityConstraintViolationError,
|
||||
* InvalidConfigurationError,
|
||||
* NotFoundError,
|
||||
* NotNullIntegrityConstraintViolationError,
|
||||
* StatementCancelledError,
|
||||
* StatementTimeoutError,
|
||||
* UnexpectedStateError,
|
||||
* UniqueIntegrityConstraintViolationError,
|
||||
* TupleMovedToAnotherPartitionError
|
||||
*
|
||||
* (reference)[https://github.com/gajus/slonik#error-handling]
|
||||
*/
|
||||
|
||||
import type { SchemaLike } from '@logto/schemas';
|
||||
import type { Middleware } from 'koa';
|
||||
import { SlonikError, NotFoundError } from 'slonik';
|
||||
import { SlonikError, NotFoundError, InvalidInputError } from 'slonik';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { DeletionError, InsertionError, UpdateError } from '#src/errors/SlonikError/index.js';
|
||||
|
@ -35,6 +14,13 @@ export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<St
|
|||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof InvalidInputError) {
|
||||
throw new RequestError({
|
||||
code: 'entity.invalid_input',
|
||||
status: 422,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof InsertionError) {
|
||||
throw new RequestError({
|
||||
code: 'entity.create_failed',
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
logtoCookieKey,
|
||||
type LogtoUiCookie,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { conditional, tryThat } from '@silverhand/essentials';
|
||||
import i18next from 'i18next';
|
||||
import koaBody from 'koa-body';
|
||||
import Provider, { errors, type ResourceServer } from 'oidc-provider';
|
||||
|
@ -216,7 +216,10 @@ export default function initOidc(
|
|||
claims: userClaims,
|
||||
// https://github.com/panva/node-oidc-provider/tree/main/docs#findaccount
|
||||
findAccount: async (_ctx, sub) => {
|
||||
const user = await findUserById(sub);
|
||||
// The user may be deleted after the token is issued
|
||||
const user = await tryThat(findUserById(sub), () => {
|
||||
throw new errors.InvalidGrant('user not found');
|
||||
});
|
||||
|
||||
return {
|
||||
accountId: sub,
|
||||
|
|
|
@ -72,6 +72,11 @@ describe('admin console user management', () => {
|
|||
expect(updatedUser).toMatchObject(newUserData);
|
||||
});
|
||||
|
||||
it('should respond 422 when no update data provided', async () => {
|
||||
const user = await createUserByAdmin();
|
||||
await expect(updateUser(user.id, {})).rejects.toMatchObject(createResponseWithCode(422));
|
||||
});
|
||||
|
||||
it('should fail when update userinfo with conflict identifiers', async () => {
|
||||
const [username, email, phone] = [generateUsername(), generateEmail(), generatePhone()];
|
||||
await createUserByAdmin(username, undefined, email, phone);
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* This file contains special error handling test cases of the main flow (sign-in experience)
|
||||
* that are not belong to any specific flow.
|
||||
*
|
||||
* For normal error handling test cases, please add them to the corresponding test files of the flow.
|
||||
*/
|
||||
|
||||
import { fetchTokenByRefreshToken } from '@logto/js';
|
||||
import { SignInIdentifier, InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { putInteraction } from '#src/api/interaction.js';
|
||||
import { defaultConfig } from '#src/client/index.js';
|
||||
import { initClient, processSession } from '#src/helpers/client.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile } from '#src/helpers/user.js';
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw invalid grant error for token endpoint when user is deleted after sign-in', async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
|
||||
const id = await processSession(client, redirectTo);
|
||||
await deleteUser(id);
|
||||
|
||||
await fetchTokenByRefreshToken(
|
||||
{
|
||||
clientId: defaultConfig.appId,
|
||||
tokenEndpoint: defaultConfig.endpoint + '/oidc/token',
|
||||
refreshToken: (await client.getRefreshToken())!,
|
||||
},
|
||||
// @ts-expect-error for testing purpose, no need to pass in a real requester
|
||||
async (...args) => {
|
||||
const response = await fetch(...args);
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toMatchObject({
|
||||
error: 'invalid_grant',
|
||||
error_description: 'grant request is invalid',
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
const entity = {
|
||||
create_failed: 'Fehler beim erstellen von {{name}}.',
|
||||
invalid_input: 'Ungültige Eingabe. Wertliste darf nicht leer sein.',
|
||||
create_failed: 'Fehler beim Erstellen von {{name}}.',
|
||||
not_exists: '{{name}} existiert nicht.',
|
||||
not_exists_with_id: '{{name}} mit ID `{{id}}` existiert nicht.',
|
||||
not_found: 'Die Ressource wurde nicht gefunden.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Invalid input. Value list must not be empty.',
|
||||
create_failed: 'Failed to create {{name}}.',
|
||||
not_exists: 'The {{name}} does not exist.',
|
||||
not_exists_with_id: 'The {{name}} with ID `{{id}}` does not exist.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada no válida. La lista de valores no debe estar vacía.',
|
||||
create_failed: 'Fallo al crear {{name}}.',
|
||||
not_exists: 'El {{name}} no existe.',
|
||||
not_exists_with_id: 'El {{name}} con ID `{{id}}` no existe.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Saisie invalide. La liste des valeurs ne doit pas être vide.',
|
||||
create_failed: 'Échec de la création de {{name}}.',
|
||||
not_exists: "Le {{name}} n'existe pas.",
|
||||
not_exists_with_id: "Le {{name}} avec l'ID `{{id}}` n'existe pas.",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Input non valido. La lista dei valori non deve essere vuota.',
|
||||
create_failed: 'Impossibile creare {{name}}.',
|
||||
not_exists: '{{name}} non esiste.',
|
||||
not_exists_with_id: '{{name}} con ID `{{id}}` non esiste.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: '入力が無効です。値のリストは空であってはなりません。',
|
||||
create_failed: '{{name}}の作成に失敗しました。',
|
||||
not_exists: '{{name}}は存在しません。',
|
||||
not_exists_with_id: 'IDが`{{id}}`の{{name}}は存在しません。',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: '입력이 잘못되었습니다. 값 목록은 비어 있을 수 없습니다.',
|
||||
create_failed: '{{name}} 생성을 실패하였어요.',
|
||||
not_exists: '{{name}}는 존재하지 않아요.',
|
||||
not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Nieprawidłowe dane. Lista wartości nie może być pusta.',
|
||||
create_failed: 'Nie udało się utworzyć {{name}}.',
|
||||
not_exists: '{{name}} nie istnieje.',
|
||||
not_exists_with_id: '{{name}} o identyfikatorze `{{id}}` nie istnieje.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada inválida. A lista de valores não deve estar vazia.',
|
||||
create_failed: 'Falha ao criar {{name}}.',
|
||||
not_exists: 'O {{name}} não existe.',
|
||||
not_exists_with_id: 'O {{name}} com ID `{{id}}` não existe.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Entrada inválida. A lista de valores não deve estar vazia.',
|
||||
create_failed: 'Falha ao criar {{name}}.',
|
||||
not_exists: '{{name}} não existe.',
|
||||
not_exists_with_id: '{{name}} com o ID `{{id}}` não existe.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Неверный ввод. Список значений не должен быть пустым.',
|
||||
create_failed: 'Не удалось создать {{name}}.',
|
||||
not_exists: '{{name}} не существует.',
|
||||
not_exists_with_id: '{{name}} с ID `{{id}}` не существует.',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: 'Geçersiz giriş. Değer listesi boş olmamalıdır.',
|
||||
create_failed: '{{name}} oluşturulamadı.',
|
||||
not_exists: '{{name}} mevcut değil.',
|
||||
not_exists_with_id: ' `{{id}}` id kimliğine sahip {{name}} mevcut değil.',
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
const entity = {
|
||||
create_failed: '创建 {{name}} 失败',
|
||||
not_exists: '该 {{name}} 不存在',
|
||||
not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在',
|
||||
not_found: '该资源不存在',
|
||||
invalid_input: '无效输入。值列表不能为空。',
|
||||
create_failed: '创建 {{name}} 失败。',
|
||||
not_exists: '该 {{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '该资源不存在。',
|
||||
};
|
||||
|
||||
export default entity;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
const entity = {
|
||||
create_failed: '創建 {{name}} 失敗',
|
||||
not_exists: '該 {{name}} 不存在',
|
||||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在',
|
||||
not_found: '該資源不存在',
|
||||
invalid_input: '無效輸入。值列表不能為空。',
|
||||
create_failed: '創建 {{name}} 失敗。',
|
||||
not_exists: '該 {{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '該資源不存在。',
|
||||
};
|
||||
|
||||
export default entity;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const entity = {
|
||||
invalid_input: '無效的輸入。值列表不能為空。',
|
||||
create_failed: '建立 {{name}} 失敗。',
|
||||
not_exists: '{{name}} 不存在。',
|
||||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
|
|
Loading…
Reference in a new issue