diff --git a/src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts b/src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts index 28cf22d..e68c8a2 100644 --- a/src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts +++ b/src/apps/napcat-webui-gateway/infrastructure/session/napcat-webui-gateway-ticket.service.ts @@ -35,14 +35,13 @@ export class NapcatWebuiGatewayTicketService { } /** - * Redeems a ticket once by deleting the Redis key before returning the session id. + * Redeems a ticket once with Redis GETDEL so deletion happens before returning. * @param ticket - Opaque ticket from the bootstrap iframe URL. * @returns Session id when the ticket existed, otherwise undefined. */ async redeem(ticket: string) { const key = this.ticketKey(ticket); - const sessionId = await this.redis.get(key); - await this.redis.del(key); + const sessionId = await this.redis.getdel(key); return sessionId || undefined; } diff --git a/test/apps/napcat-webui-gateway/session-store.spec.ts b/test/apps/napcat-webui-gateway/session-store.spec.ts index 5159d8d..23924a2 100644 --- a/test/apps/napcat-webui-gateway/session-store.spec.ts +++ b/test/apps/napcat-webui-gateway/session-store.spec.ts @@ -116,6 +116,19 @@ class FakeRedis { return this.values.get(key) ?? null; } + /** + * Atomically reads and deletes one key from the fake Redis store. + * @param key - Redis key to consume. + * @returns Stored value before deletion or null. + */ + async getdel(key: string) { + this.calls.push(`getdel:${key}`); + const value = this.values.get(key) ?? null; + this.values.delete(key); + this.ttl.delete(key); + return value; + } + /** * Deletes one or more keys from the fake Redis store. * @param keys - Redis keys to delete. @@ -318,13 +331,16 @@ describe('NapcatWebuiGatewayTicketService', () => { expect(sessionId).toBe('session-1'); expect(secondRedeem).toBeUndefined(); - expect(redis.values.get(`napcat:webui:ticket:${ticket}`)).toBeUndefined(); - expect(redis.calls).toEqual( - expect.arrayContaining([ - `get:napcat:webui:ticket:${ticket}`, - `del:napcat:webui:ticket:${ticket}`, - ]), - ); + const ticketKey = `napcat:webui:ticket:${ticket}`; + const ticketCalls = redis.calls.filter((call) => call.includes(ticketKey)); + expect(redis.values.get(ticketKey)).toBeUndefined(); + expect(ticketCalls).toEqual([ + `set:${ticketKey}:PX:60000`, + `getdel:${ticketKey}`, + `getdel:${ticketKey}`, + ]); + expect(ticketCalls).not.toContain(`get:${ticketKey}`); + expect(ticketCalls).not.toContain(`del:${ticketKey}`); }); });