nexu-io--open-design
169 行
5.3 KiB
TypeScript
169 行
5.3 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import { resolveXAIBearer } from '../../../src/integrations/xai-credentials.js';
|
|
import { setXAIToken, type StoredXAIToken } from '../../../src/integrations/xai-tokens.js';
|
|
|
|
type FetchInput = Parameters<typeof fetch>[0];
|
|
type FetchInit = Parameters<typeof fetch>[1];
|
|
|
|
function fetchOk(body: unknown, status = 200) {
|
|
return async (_input: FetchInput, _init?: FetchInit): Promise<Response> => {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
};
|
|
}
|
|
|
|
function fetchAlwaysFails(status = 400, body = '{"error":"invalid_grant"}') {
|
|
return async (): Promise<Response> => {
|
|
return new Response(body, {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
};
|
|
}
|
|
|
|
describe('resolveXAIBearer', () => {
|
|
let dataDir: string;
|
|
|
|
beforeEach(async () => {
|
|
dataDir = await mkdtemp(path.join(tmpdir(), 'od-xai-creds-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(dataDir, { force: true, recursive: true });
|
|
});
|
|
|
|
it('returns null when nothing is stored', async () => {
|
|
expect(await resolveXAIBearer(dataDir)).toBeNull();
|
|
});
|
|
|
|
it('returns the stored token unchanged when not yet within the skew window', async () => {
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'fresh-bearer',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now(),
|
|
expiresAt: Date.now() + 3_600_000,
|
|
refreshToken: 'rt-1',
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
const resolved = await resolveXAIBearer(dataDir);
|
|
expect(resolved).toEqual({
|
|
accessToken: 'fresh-bearer',
|
|
source: 'stored',
|
|
});
|
|
});
|
|
|
|
it('returns the stored token unchanged when expiresAt is missing', async () => {
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'never-expires',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now(),
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
const resolved = await resolveXAIBearer(dataDir);
|
|
expect(resolved).toEqual({
|
|
accessToken: 'never-expires',
|
|
source: 'stored',
|
|
});
|
|
});
|
|
|
|
it('refreshes a token within the skew window and writes the new value back', async () => {
|
|
const oldExpiry = Date.now() + 30_000; // within 120 s skew
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'old-bearer',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now(),
|
|
expiresAt: oldExpiry,
|
|
refreshToken: 'rt-1',
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
const resolved = await resolveXAIBearer(
|
|
dataDir,
|
|
fetchOk({
|
|
access_token: 'new-bearer',
|
|
refresh_token: 'rt-2',
|
|
token_type: 'Bearer',
|
|
expires_in: 1800,
|
|
}),
|
|
);
|
|
expect(resolved).toEqual({
|
|
accessToken: 'new-bearer',
|
|
source: 'refreshed',
|
|
});
|
|
|
|
const onDisk = JSON.parse(
|
|
await readFile(path.join(dataDir, 'xai-tokens.json'), 'utf8'),
|
|
);
|
|
expect(onDisk.token.accessToken).toBe('new-bearer');
|
|
expect(onDisk.token.refreshToken).toBe('rt-2');
|
|
// New expiry should be ~ now + 1800s (with some margin for test latency).
|
|
const expectedMin = Date.now() + 1800 * 1000 - 10_000;
|
|
expect(onDisk.token.expiresAt).toBeGreaterThanOrEqual(expectedMin);
|
|
});
|
|
|
|
it('returns null when the stored token is expired and has no refresh_token', async () => {
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'expired-bearer',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now() - 7_200_000,
|
|
expiresAt: Date.now() - 60_000,
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
expect(await resolveXAIBearer(dataDir)).toBeNull();
|
|
});
|
|
|
|
it('returns null when refresh fails (network or 4xx)', async () => {
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'old-bearer',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now(),
|
|
expiresAt: Date.now() + 30_000,
|
|
refreshToken: 'rt-stale',
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
const resolved = await resolveXAIBearer(dataDir, fetchAlwaysFails(400));
|
|
expect(resolved).toBeNull();
|
|
});
|
|
|
|
it('preserves the existing refresh_token when the refresh response omits it', async () => {
|
|
const stored: StoredXAIToken = {
|
|
accessToken: 'old-bearer',
|
|
tokenType: 'Bearer',
|
|
savedAt: Date.now(),
|
|
expiresAt: Date.now() + 30_000,
|
|
refreshToken: 'rt-1',
|
|
};
|
|
await setXAIToken(dataDir, stored);
|
|
|
|
const resolved = await resolveXAIBearer(
|
|
dataDir,
|
|
// Some token endpoints omit refresh_token in the refresh response
|
|
// (RFC 6749 §6 lets the server keep the old one valid).
|
|
// resolveXAIBearer must carry the previous refresh_token forward
|
|
// so the next expiry can still refresh.
|
|
fetchOk({ access_token: 'new-bearer', token_type: 'Bearer' }),
|
|
);
|
|
expect(resolved?.accessToken).toBe('new-bearer');
|
|
expect(resolved?.source).toBe('refreshed');
|
|
|
|
const onDisk = JSON.parse(
|
|
await readFile(path.join(dataDir, 'xai-tokens.json'), 'utf8'),
|
|
);
|
|
// Old refresh_token must survive the partial refresh response so
|
|
// the next expiry doesn't kick the user back through Sign in.
|
|
expect(onDisk.token.refreshToken).toBe('rt-1');
|
|
// expires_in not in response → we don't fabricate one.
|
|
expect(onDisk.token.expiresAt).toBeUndefined();
|
|
});
|
|
});
|