2026-09-09 17:51:39 +03:00
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
|
import { fetchQuota, isConfigured } from './cline-pass.js';
|
2026-09-08 17:02:56 +02:00
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
const readAuth = () => ({ 'cline-pass': { key: 'test-token' } });
|
2026-09-08 17:02:56 +02:00
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
// Response shape verified by the contributor against the live ClinePass API.
|
2026-09-08 17:02:56 +02:00
|
|
|
const documentedPayload = {
|
2026-09-09 17:51:39 +03:00
|
|
|
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', () => {
|
2026-09-09 17:51:39 +03:00
|
|
|
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();
|
2026-09-09 17:51:39 +03:00
|
|
|
expect(JSON.stringify(result)).not.toContain('test-token');
|
2026-09-08 17:02:56 +02:00
|
|
|
});
|
|
|
|
|
|
2026-09-09 17:51:39 +03: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
|
|
|
|
2026-09-09 17:51:39 +03: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
|
|
|
|
2026-09-09 17:51:39 +03: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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2026-09-09 17:51:39 +03: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);
|
2026-09-09 17:51:39 +03:00
|
|
|
expect(result.usage).toBeNull();
|
|
|
|
|
expect(result.error).toBe(error);
|
2026-09-08 17:02:56 +02:00
|
|
|
});
|
|
|
|
|
|
2026-09-09 17:51:39 +03: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
|
|
|
|
2026-09-09 17:51:39 +03: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);
|
2026-09-09 17:51:39 +03:00
|
|
|
expect(result.configured).toBe(true);
|
|
|
|
|
expect(result.usage).toBeNull();
|
|
|
|
|
expect(result.error).toBe('Request timed out');
|
2026-09-08 17:02:56 +02:00
|
|
|
});
|
|
|
|
|
});
|