fix: 原子化NapCat WebUI票据兑换

This commit is contained in:
sunlei 2026-06-24 13:11:17 +08:00
parent 6f23db64be
commit 8ee7866afe
2 changed files with 25 additions and 10 deletions

View File

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

View File

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