Files
Bohdan Triapitsyn b0282b2720 fix(quota): validate Hyper credentials and clean up balance labels
Reject invalid credentials while preserving valid token fallback, parse balances with existing boundary helpers, and keep credit values free of untranslated unit text. Inject auth and HTTP dependencies for focused tests in both runtimes.

Validated web quota and registry tests (35 passed), VS Code quota tests (70 passed), both package type checks and lint, extension build, and changed-line anti-slop checks. Reviewed dead-code output. Live Hyper validation was not run because no API key is available.
2026-09-05 23:26:35 +03:00

127 lines
4.7 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import { fetchQuota, isConfigured } from './hyper.js';
const readAuth = () => ({ hyper: { key: 'test-token' } });
// https://hyper.charm.land/docs/api/credits.html documents the balance payload.
// https://hyper.charm.land/faq defines one Hypercredit as $0.05.
describe('Charm Hyper quota provider', () => {
it.each([
[100, '100', '$5.00'],
['50', '50', '$2.50'],
[25.5, '25.50', '$1.28'],
[0, '0', '$0.00'],
['0', '0', '$0.00'],
])('formats balance %s without an untranslated unit', async (balance, credits, dollars) => {
const result = await fetchQuota({
readAuth,
fetchImpl: async () => Response.json({ balance }),
});
expect(result.ok).toBe(true);
expect(result.providerId).toBe('hyper');
expect(result.configured).toBe(true);
expect(result.usage.windows.credits.valueLabel).toBe(credits);
expect(result.usage.windows.credits_balance.valueLabel).toBe(dollars);
for (const window of Object.values(result.usage.windows)) {
expect(window.usedPercent).toBeNull();
expect(window.remainingPercent).toBeNull();
expect(window.windowSeconds).toBeNull();
expect(window.resetAt).toBeNull();
expect(window.resetAfterSeconds).toBeNull();
}
});
it.each([
{}, null, [], { balance: '' }, { balance: ' \t ' }, { balance: 'NaN' },
{ balance: 'Infinity' }, { balance: null }, { balance: true }, { balance: [] },
{ balance: {} },
])('rejects invalid payload %j instead of showing zero', async (payload) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => Response.json(payload) });
expect(result.ok).toBe(false);
expect(result.configured).toBe(true);
expect(result.error).toBe('No quota data in response');
expect(result.usage).toBeNull();
});
it.each([
{ hyper: { key: 'test-token' } },
{ hyper: { token: 'test-token' } },
{ hyper: 'test-token' },
{ hyper: { key: ' ', token: 'test-token' } },
{ hyper: { key: 42, token: 'test-token' } },
])('uses a validated credential for the documented request', async (auth) => {
expect(isConfigured(auth)).toBe(true);
let requests = 0;
const result = await fetchQuota({
readAuth: () => auth,
fetchImpl: async (url, options) => {
requests += 1;
expect(url).toBe('https://hyper.charm.land/v1/credits');
expect(options.method).toBe('GET');
expect(new Headers(options.headers).get('Authorization')).toBe('Bearer test-token');
expect(options.signal).toBeInstanceOf(AbortSignal);
return Response.json({ balance: 100 });
},
});
expect(requests).toBe(1);
expect(result.ok).toBe(true);
expect(JSON.stringify(result)).not.toContain('test-token');
});
it.each([{}, { hyper: { key: '' } }, { hyper: { key: ' ' } }, { hyper: { key: 42 } }])(
'does not request usage without a valid credential',
async (auth) => {
expect(isConfigured(auth)).toBe(false);
let requests = 0;
const result = await fetchQuota({
readAuth: () => auth,
fetchImpl: async () => {
requests += 1;
return Response.json({ balance: 100 });
},
});
expect(requests).toBe(0);
expect(result.ok).toBe(false);
expect(result.configured).toBe(false);
expect(result.error).toBe('Not configured');
},
);
it.each([
[401, 'Session expired — please re-authenticate with Charm Hyper'],
[403, 'Session expired — please re-authenticate with Charm Hyper'],
[429, 'API error: 429'],
[500, 'API error: 500'],
])('reports HTTP %s as a failure', async (status, error) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response(null, { status }) });
expect(result.ok).toBe(false);
expect(result.configured).toBe(true);
expect(result.error).toBe(error);
expect(result.usage).toBeNull();
});
it('reports invalid JSON as a parse failure', async () => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => new Response('{') });
expect(result.error).toBe('Invalid response from provider');
expect(result.ok).toBe(false);
expect(result.configured).toBe(true);
expect(result.usage).toBeNull();
});
it.each([
[new DOMException('Timed out', 'TimeoutError'), 'Request timed out'],
[new Error('Network unavailable'), 'Network unavailable'],
])('reports request failure', async (failure, message) => {
const result = await fetchQuota({ readAuth, fetchImpl: async () => { throw failure; } });
expect(result.error).toBe(message);
expect(result.ok).toBe(false);
expect(result.configured).toBe(true);
expect(result.usage).toBeNull();
});
});