mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
`getValidAccessToken` handed back the access token it had just found expired whenever there was no refresh token to spend. Every caller reads a non-null token as "signed in", so the answer surface searched with a dead bearer and rendered the 401 as a failed search rather than as a session that had ended. A token read answers one question — is there a token this client may use right now — and "expired, unrefreshable" is a no. It now clears the session and returns null, so the surfaces ask for a sign-in instead of discovering the refusal a request later. A token whose expiry was never recorded is untouched: the direct-token login path stores no `expires_in`, so absence of an expiry is not evidence of staleness. Seven cases, two of which fail against the previous implementation. Co-authored-by: Hanzo Dev <[email protected]>
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { getValidAccessToken } from '../src/shared/auth';
|
|
import { STORAGE_KEYS } from '../src/shared/config';
|
|
|
|
/**
|
|
* A token read answers ONE question: is there a token this client may use right
|
|
* now? Anything else — expired, unrefreshable — is "no", and the caller signs in.
|
|
* The surfaces are built on that answer: the answer engine treats a non-null
|
|
* token as signed in, so a stale one turns a signed-out session into a search
|
|
* that fails with a 401 the user cannot act on.
|
|
*/
|
|
|
|
const HOUR = 3_600_000;
|
|
|
|
/** A storage double over a plain record — the same shape BrowserStorage exposes. */
|
|
function storageOf(seed: Record<string, unknown>) {
|
|
const data = { ...seed };
|
|
return {
|
|
data,
|
|
async get(keys: string[]) {
|
|
return Object.fromEntries(keys.filter((k) => k in data).map((k) => [k, data[k]]));
|
|
},
|
|
async set(next: Record<string, unknown>) {
|
|
Object.assign(data, next);
|
|
},
|
|
async remove(keys: string[]) {
|
|
for (const k of keys) delete data[k];
|
|
},
|
|
};
|
|
}
|
|
|
|
const mockFetch = vi.fn();
|
|
vi.stubGlobal('fetch', mockFetch);
|
|
|
|
beforeEach(() => mockFetch.mockReset());
|
|
|
|
describe('getValidAccessToken', () => {
|
|
it('returns a token that has not expired', async () => {
|
|
const storage = storageOf({
|
|
[STORAGE_KEYS.accessToken]: 'fresh',
|
|
[STORAGE_KEYS.expiresAt]: Date.now() + HOUR,
|
|
});
|
|
expect(await getValidAccessToken(storage as any)).toBe('fresh');
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('refuses an expired token that cannot be refreshed, and clears the session', async () => {
|
|
const storage = storageOf({
|
|
[STORAGE_KEYS.accessToken]: 'stale',
|
|
[STORAGE_KEYS.expiresAt]: Date.now() - HOUR,
|
|
});
|
|
expect(await getValidAccessToken(storage as any)).toBeNull();
|
|
// Nothing left to make a later read believe it is still signed in.
|
|
expect(storage.data[STORAGE_KEYS.accessToken]).toBeUndefined();
|
|
});
|
|
|
|
it('treats a token inside the last minute of its life as expired', async () => {
|
|
const storage = storageOf({
|
|
[STORAGE_KEYS.accessToken]: 'about-to-die',
|
|
[STORAGE_KEYS.expiresAt]: Date.now() + 30_000,
|
|
});
|
|
expect(await getValidAccessToken(storage as any)).toBeNull();
|
|
});
|
|
|
|
it('refreshes an expired token when a refresh token is stored', async () => {
|
|
mockFetch.mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ access_token: 'minted', token_type: 'Bearer', expires_in: 3600 }),
|
|
});
|
|
const storage = storageOf({
|
|
[STORAGE_KEYS.accessToken]: 'stale',
|
|
[STORAGE_KEYS.refreshToken]: 'refresh-me',
|
|
[STORAGE_KEYS.expiresAt]: Date.now() - HOUR,
|
|
});
|
|
expect(await getValidAccessToken(storage as any)).toBe('minted');
|
|
expect(storage.data[STORAGE_KEYS.accessToken]).toBe('minted');
|
|
});
|
|
|
|
it('clears the session when the refresh is rejected', async () => {
|
|
mockFetch.mockResolvedValue({ ok: false, status: 400, json: async () => ({}) });
|
|
const storage = storageOf({
|
|
[STORAGE_KEYS.accessToken]: 'stale',
|
|
[STORAGE_KEYS.refreshToken]: 'revoked',
|
|
[STORAGE_KEYS.expiresAt]: Date.now() - HOUR,
|
|
});
|
|
expect(await getValidAccessToken(storage as any)).toBeNull();
|
|
expect(storage.data[STORAGE_KEYS.refreshToken]).toBeUndefined();
|
|
});
|
|
|
|
it('keeps a token whose expiry was never recorded', async () => {
|
|
// The direct-token login path stores no expires_in, so an absent expiry is
|
|
// not evidence of staleness.
|
|
const storage = storageOf({ [STORAGE_KEYS.accessToken]: 'no-expiry-known' });
|
|
expect(await getValidAccessToken(storage as any)).toBe('no-expiry-known');
|
|
});
|
|
|
|
it('is null when nothing is stored', async () => {
|
|
expect(await getValidAccessToken(storageOf({}) as any)).toBeNull();
|
|
});
|
|
});
|