From 6058231a1d8d9a9bb4f55e77299d9569bb6ab945 Mon Sep 17 00:00:00 2001 From: ding113 Date: Mon, 3 Aug 2026 22:04:16 +0800 Subject: [PATCH] fix(keys): preserve last-enabled-key error code --- src/actions/keys.ts | 8 ++++---- src/lib/utils/error-messages.ts | 1 + .../actions/keys-self-service-authz.test.ts | 14 +++++++++++++ tests/unit/api/v1/api-client-actions.test.ts | 20 +++++++++++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/actions/keys.ts b/src/actions/keys.ts index 1b9241b30..23dd7ee52 100644 --- a/src/actions/keys.ts +++ b/src/actions/keys.ts @@ -505,7 +505,7 @@ export async function editKey( return { ok: false, error: tError("CANNOT_DISABLE_LAST_KEY"), - errorCode: ERROR_CODES.OPERATION_FAILED, + errorCode: ERROR_CODES.CANNOT_DISABLE_LAST_KEY, }; } } @@ -1263,7 +1263,7 @@ export async function toggleKeyEnabled(keyId: number, enabled: boolean): Promise return { ok: false, error: tError("CANNOT_DISABLE_LAST_KEY"), - errorCode: ERROR_CODES.OPERATION_FAILED, + errorCode: ERROR_CODES.CANNOT_DISABLE_LAST_KEY, }; } } @@ -1418,7 +1418,7 @@ export async function batchUpdateKeys( if (currentEnabledCount - disableCount < 1) { throw new BatchUpdateError( tError("CANNOT_DISABLE_LAST_KEY"), - ERROR_CODES.OPERATION_FAILED + ERROR_CODES.CANNOT_DISABLE_LAST_KEY ); } } @@ -1478,7 +1478,7 @@ export async function batchUpdateKeys( if (Number(remainingEnabled?.count ?? 0) < 1) { throw new BatchUpdateError( tError("CANNOT_DISABLE_LAST_KEY"), - ERROR_CODES.OPERATION_FAILED + ERROR_CODES.CANNOT_DISABLE_LAST_KEY ); } } diff --git a/src/lib/utils/error-messages.ts b/src/lib/utils/error-messages.ts index eb71f53d5..3c59e283d 100644 --- a/src/lib/utils/error-messages.ts +++ b/src/lib/utils/error-messages.ts @@ -101,6 +101,7 @@ export const BUSINESS_ERRORS = { USER_LIMITS_RESET_PARTIAL_FAILURE: "USER_LIMITS_RESET_PARTIAL_FAILURE", USER_STATS_RESET_PARTIAL_FAILURE: "USER_STATS_RESET_PARTIAL_FAILURE", CANNOT_DELETE_LAST_KEY: "CANNOT_DELETE_LAST_KEY", + CANNOT_DISABLE_LAST_KEY: "CANNOT_DISABLE_LAST_KEY", CANNOT_DELETE_LAST_GROUP_KEY: "CANNOT_DELETE_LAST_GROUP_KEY", KEY_NOT_FOUND: "KEY_NOT_FOUND", } as const; diff --git a/tests/unit/actions/keys-self-service-authz.test.ts b/tests/unit/actions/keys-self-service-authz.test.ts index 236a4bf95..391d1c210 100644 --- a/tests/unit/actions/keys-self-service-authz.test.ts +++ b/tests/unit/actions/keys-self-service-authz.test.ts @@ -193,6 +193,20 @@ describe("toggleKeyEnabled self-service authorization", () => { expect(result.ok).toBe(true); expect(updateKeyMock).toHaveBeenCalledWith(42, { is_enabled: false }); }); + + it("returns the dedicated business code when disabling the last enabled key", async () => { + getSessionMock.mockResolvedValue(webSession); + countActiveKeysByUserMock.mockResolvedValue(1); + + const { toggleKeyEnabled } = await import("@/actions/keys"); + const result = await toggleKeyEnabled(42, false); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errorCode).toBe("CANNOT_DISABLE_LAST_KEY"); + } + expect(updateKeyMock).not.toHaveBeenCalled(); + }); }); describe("renewKeyExpiresAt self-service authorization", () => { diff --git a/tests/unit/api/v1/api-client-actions.test.ts b/tests/unit/api/v1/api-client-actions.test.ts index 36307abee..e8884a418 100644 --- a/tests/unit/api/v1/api-client-actions.test.ts +++ b/tests/unit/api/v1/api-client-actions.test.ts @@ -598,6 +598,26 @@ describe("v1 action compatibility client", () => { }); }); + test("preserves the last-enabled-key business code through toggleKeyEnabled", async () => { + postMock.mockRejectedValueOnce( + new ApiError({ + status: 400, + errorCode: "CANNOT_DISABLE_LAST_KEY", + detail: "Bad request", + }) + ); + + const result = await keys.toggleKeyEnabled(7, false); + + expect(postMock).toHaveBeenCalledWith("/api/v1/keys/7:enable", { enabled: false }, undefined); + expect(result).toEqual({ + ok: false, + error: "Bad request", + errorCode: "CANNOT_DISABLE_LAST_KEY", + errorParams: undefined, + }); + }); + test("maps key.action_failed through toVoidActionResult to OPERATION_FAILED", async () => { deleteMock.mockRejectedValueOnce( new ApiError({ status: 400, errorCode: "key.action_failed", detail: "Bad request" })