0
Fork 0
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:
Gao Sun 2023-07-22 17:32:25 +08:00 committed by GitHub
parent bc2feb6c74
commit 6d1ea26cdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 101 additions and 33 deletions

View file

@ -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',

View file

@ -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,

View file

@ -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);

View file

@ -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',
});
}
);
});
});

View file

@ -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.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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.",

View file

@ -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.',

View file

@ -1,4 +1,5 @@
const entity = {
invalid_input: '入力が無効です。値のリストは空であってはなりません。',
create_failed: '{{name}}の作成に失敗しました。',
not_exists: '{{name}}は存在しません。',
not_exists_with_id: 'IDが`{{id}}`の{{name}}は存在しません。',

View file

@ -1,4 +1,5 @@
const entity = {
invalid_input: '입력이 잘못되었습니다. 값 목록은 비어 있을 수 없습니다.',
create_failed: '{{name}} 생성을 실패하였어요.',
not_exists: '{{name}}는 존재하지 않아요.',
not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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.',

View file

@ -1,4 +1,5 @@
const entity = {
invalid_input: 'Неверный ввод. Список значений не должен быть пустым.',
create_failed: 'Не удалось создать {{name}}.',
not_exists: '{{name}} не существует.',
not_exists_with_id: '{{name}} с ID `{{id}}` не существует.',

View file

@ -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.',

View file

@ -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;

View file

@ -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;

View file

@ -1,4 +1,5 @@
const entity = {
invalid_input: '無效的輸入。值列表不能為空。',
create_failed: '建立 {{name}} 失敗。',
not_exists: '{{name}} 不存在。',
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',