Files

104 lines
5.3 KiB
JavaScript
Raw Permalink Normal View History

import { describe, expect, it } from 'vitest';
import { fetchQuota, isConfigured } from './cline-pass.js';
2026-09-08 17:02:56 +02:00
const readAuth = () => ({ 'cline-pass': { key: 'test-token' } });
2026-09-08 17:02:56 +02:00
// Response shape verified by the contributor against the live ClinePass API.
2026-09-08 17:02:56 +02:00
const documentedPayload = {
data: { limits: [
{ type: 'five_hour', percentUsed: 43, resetsAt: '2026-09-08T17:00:44.598174595Z' },
{ type: 'weekly', percentUsed: 17 },
{ type: 'monthly', percentUsed: 8 },
] },
2026-09-08 17:02:56 +02:00
success: true,
};
describe('ClinePass quota provider', () => {
it('maps the documented windows and sends credentials only in headers', async () => {
const result = await fetchQuota({ readAuth, fetchImpl: async (url, options) => {
expect(url).toBe('https://api.cline.bot/api/v1/users/me/plan/usage-limits');
expect(new Headers(options.headers).get('Authorization')).toBe('Bearer test-token');
expect(options.signal).toBeInstanceOf(AbortSignal);
return Response.json(documentedPayload);
} });
2026-09-08 17:02:56 +02:00
expect(result.ok).toBe(true);
expect(result.providerId).toBe('cline-pass');
expect(Object.keys(result.usage.windows)).toEqual(['5h', 'weekly', 'monthly']);
expect(result.usage.windows['5h'].usedPercent).toBe(43);
expect(result.usage.windows['5h'].remainingPercent).toBe(57);
expect(result.usage.windows['5h'].windowSeconds).toBe(18_000);
expect(result.usage.windows['5h'].resetAt).toBe(Date.parse('2026-09-08T17:00:44.598174595Z'));
expect(result.usage.windows.weekly.usedPercent).toBe(17);
expect(result.usage.windows.weekly.windowSeconds).toBe(604_800);
expect(result.usage.windows.monthly.usedPercent).toBe(8);
expect(result.usage.windows.monthly.windowSeconds).toBeNull();
expect(JSON.stringify(result)).not.toContain('test-token');
2026-09-08 17:02:56 +02:00
});
it.each([0, '0', '51'])('accepts finite percentage %s', async (percentUsed) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => Response.json({ data: { limits: [{ type: 'weekly', percentUsed }] } }) });
expect(result.ok).toBe(true);
expect(result.usage.windows.weekly.usedPercent).toBe(Number(percentUsed));
});
2026-09-08 17:02:56 +02:00
it.each([
{ type: 'constructor', percentUsed: 5 }, { type: 'toString', percentUsed: 5 },
{ type: '__proto__', percentUsed: 5 }, { type: 'quarterly', percentUsed: 5 },
{ type: 'weekly', percentUsed: '' }, { type: 'weekly', percentUsed: ' ' },
{ type: 'weekly', percentUsed: null }, { type: 'weekly', percentUsed: true },
{ type: 'weekly', percentUsed: 'NaN' }, null,
])('ignores malformed windows without discarding valid siblings: %j', async (limit) => {
const invalid = await fetchQuota({ readAuth, fetchImpl: async () => Response.json({ data: { limits: [limit] } }) });
expect(invalid.ok).toBe(false);
expect(invalid.configured).toBe(true);
expect(invalid.usage).toBeNull();
const mixed = await fetchQuota({ readAuth, fetchImpl: async () => Response.json({ data: { limits: [limit, { type: 'monthly', percentUsed: 8 }] } }) });
expect(mixed.ok).toBe(true);
expect(Object.keys(mixed.usage.windows)).toEqual(['monthly']);
});
2026-09-08 17:02:56 +02:00
it.each([null, [], {}, { data: null }, { data: { limits: [] } }])('rejects empty or malformed payload %j', async (payload) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => Response.json(payload) });
2026-09-08 17:02:56 +02:00
expect(result.ok).toBe(false);
expect(result.usage).toBeNull();
expect(result.error).toBe('No quota data in response');
});
it.each([{ key: '' }, { key: ' ' }, { key: 42 }, {}])('uses a usable token when the key is malformed: %j', async (entry) => {
const auth = { 'cline-pass': { ...entry, token: 'test-token' } };
expect(isConfigured(auth)).toBe(true);
const result = await fetchQuota({ readAuth: () => auth, fetchImpl: async (_url, options) => {
expect(new Headers(options.headers).get('Authorization')).toBe('Bearer test-token');
return Response.json(documentedPayload);
} });
2026-09-08 17:02:56 +02:00
expect(result.ok).toBe(true);
});
it.each([{}, { 'cline-pass': { key: '' } }, { 'cline-pass': { key: 42 } }])('does not fetch without usable credentials: %j', async (auth) => {
expect(isConfigured(auth)).toBe(false);
const result = await fetchQuota({ readAuth: () => auth, fetchImpl: async () => { throw new Error('Unexpected fetch'); } });
expect(result.configured).toBe(false);
expect(result.error).toBe('Not configured');
2026-09-08 17:02:56 +02:00
});
it.each([[401, 'Session expired — please re-authenticate with ClinePass'], [503, 'API error: 503']])('reports HTTP %s', async (status, error) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response(null, { status }) });
2026-09-08 17:02:56 +02:00
expect(result.ok).toBe(false);
expect(result.usage).toBeNull();
expect(result.error).toBe(error);
2026-09-08 17:02:56 +02:00
});
it('reports invalid JSON', async () => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response('{') });
expect(result.error).toBe('Invalid response from provider');
});
2026-09-08 17:02:56 +02:00
it('recognizes the timeout exception from AbortSignal.timeout', async () => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => { throw new DOMException('Timed out', 'TimeoutError'); } });
2026-09-08 17:02:56 +02:00
expect(result.ok).toBe(false);
expect(result.configured).toBe(true);
expect(result.usage).toBeNull();
expect(result.error).toBe('Request timed out');
2026-09-08 17:02:56 +02:00
});
});