diff --git a/src/server/routes/api/downstreamApiKeys.test.ts b/src/server/routes/api/downstreamApiKeys.test.ts index 8a59c30..cd5610d 100644 --- a/src/server/routes/api/downstreamApiKeys.test.ts +++ b/src/server/routes/api/downstreamApiKeys.test.ts @@ -314,6 +314,44 @@ describe('downstream api keys routes', () => { }); }); + it('rejects duplicate key update with conflict status', async () => { + const firstRes = await app.inject({ + method: 'POST', + url: '/api/downstream-keys', + payload: { + name: 'dup-a', + key: 'sk-dup-key-001', + }, + }); + expect(firstRes.statusCode).toBe(200); + + const secondRes = await app.inject({ + method: 'POST', + url: '/api/downstream-keys', + payload: { + name: 'dup-b', + key: 'sk-dup-key-002', + }, + }); + expect(secondRes.statusCode).toBe(200); + + const secondId = secondRes.json().item.id as number; + const duplicateUpdateRes = await app.inject({ + method: 'PUT', + url: `/api/downstream-keys/${secondId}`, + payload: { + name: 'dup-b-updated', + key: 'sk-dup-key-001', + }, + }); + + expect(duplicateUpdateRes.statusCode).toBe(409); + expect(duplicateUpdateRes.json()).toMatchObject({ + success: false, + message: 'API key 已存在', + }); + }); + it('returns summary, overview and trend aggregated from proxy logs', async () => { const inserted = await db.insert(schema.downstreamApiKeys).values({ name: 'analytics-key', diff --git a/src/server/routes/api/downstreamApiKeys.ts b/src/server/routes/api/downstreamApiKeys.ts index 78afe1e..c48c273 100644 --- a/src/server/routes/api/downstreamApiKeys.ts +++ b/src/server/routes/api/downstreamApiKeys.ts @@ -20,13 +20,42 @@ function validateKeyShape(key: string): boolean { return key.startsWith('sk-') && key.length >= 6; } -function looksLikeUniqueViolation(error: unknown): boolean { - const message = (error as Error | undefined)?.message || ''; - if (runtimeDbDialect === 'postgres') { - return message.includes('duplicate key value violates unique constraint') && - message.includes('downstream_api_keys_key_unique'); +type ErrorLike = { + message?: string; + code?: string | number; + cause?: unknown; +}; + +function getErrorChain(error: unknown): ErrorLike[] { + const chain: ErrorLike[] = []; + const seen = new Set(); + let current: unknown = error; + while (current && typeof current === 'object' && !seen.has(current)) { + seen.add(current); + chain.push(current as ErrorLike); + current = (current as ErrorLike).cause; } - return message.includes('UNIQUE constraint failed') && message.includes('downstream_api_keys.key'); + return chain; +} + +function looksLikeUniqueViolation(error: unknown): boolean { + const chain = getErrorChain(error); + if (runtimeDbDialect === 'postgres') { + return chain.some((entry) => { + const message = entry.message || ''; + const code = String(entry.code || ''); + return code === '23505' + || (message.includes('duplicate key value violates unique constraint') + && message.includes('downstream_api_keys_key_unique')); + }); + } + return chain.some((entry) => { + const message = entry.message || ''; + const code = String(entry.code || ''); + return code === 'SQLITE_CONSTRAINT' + || code === 'SQLITE_CONSTRAINT_UNIQUE' + || (message.includes('UNIQUE constraint failed') && message.includes('downstream_api_keys.key')); + }); } function normalizeBatchIds(raw: unknown): number[] {