import { test } from 'node:test'; import assert from 'node:assert/strict'; import { ApiError, chunk, classifyError, errorReasons, nextQuotaReset, parseIsoDuration, pickThumbnail, } from '../src/lib/api.js'; test('chunk splits into batches and keeps the remainder', () => { assert.deepEqual(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]); assert.deepEqual(chunk([], 50), []); assert.equal(chunk(Array.from({ length: 120 }, (_, i) => i), 50).map((b) => b.length).join(), '50,50,20'); }); test('parseIsoDuration converts ISO 8601 durations to seconds', () => { assert.equal(parseIsoDuration('PT1H2M3S'), 3723); assert.equal(parseIsoDuration('PT45S'), 45); assert.equal(parseIsoDuration('PT10M'), 600); assert.equal(parseIsoDuration('P1DT1H'), 90000); assert.equal(parseIsoDuration('P0D'), 0); assert.equal(parseIsoDuration('PT0S'), 0); }); test('parseIsoDuration returns null for garbage', () => { assert.equal(parseIsoDuration('garbage'), null); assert.equal(parseIsoDuration(undefined), null); assert.equal(parseIsoDuration(null), null); }); test('pickThumbnail prefers the largest available size', () => { const all = { default: { url: 'd' }, medium: { url: 'm' }, high: { url: 'h' }, standard: { url: 's' }, maxres: { url: 'x' }, }; assert.equal(pickThumbnail(all), 'x'); delete all.maxres; assert.equal(pickThumbnail(all), 's'); assert.equal(pickThumbnail({ default: { url: 'd' }, medium: { url: 'm' } }), 'm'); assert.equal(pickThumbnail({}), null); assert.equal(pickThumbnail(undefined), null); assert.equal(pickThumbnail(null), null); }); test('errorReasons collects reasons from errors[] and details[]', () => { const body = { error: { errors: [{ reason: 'badRequest' }], details: [{ reason: 'API_KEY_INVALID' }] } }; assert.deepEqual(errorReasons(body), ['badRequest', 'API_KEY_INVALID']); assert.deepEqual(errorReasons(null), []); assert.deepEqual(errorReasons({}), []); }); test('classifyError recognises quota, key and server problems', () => { assert.equal(classifyError(403, { error: { errors: [{ reason: 'quotaExceeded' }] } }), 'quota'); assert.equal(classifyError(403, { error: { errors: [{ reason: 'dailyLimitExceeded' }] } }), 'quota'); assert.equal( classifyError(400, { error: { errors: [{ reason: 'badRequest' }], details: [{ reason: 'API_KEY_INVALID' }] } }), 'invalid-key', ); assert.equal(classifyError(403, { error: { errors: [{ reason: 'accessNotConfigured' }] } }), 'invalid-key'); assert.equal(classifyError(500, null), 'server'); assert.equal(classifyError(503, {}), 'server'); assert.equal(classifyError(429, { error: { errors: [{ reason: 'rateLimitExceeded' }] } }), 'server'); assert.equal(classifyError(404, null), 'other'); }); test('ApiError carries its kind and status', () => { const err = new ApiError('quota', 'out of quota', 403); assert.ok(err instanceof Error); assert.equal(err.name, 'ApiError'); assert.equal(err.kind, 'quota'); assert.equal(err.status, 403); assert.equal(err.message, 'out of quota'); }); test('nextQuotaReset points at the next Pacific midnight (summer time)', () => { const noon = new Date('2026-10-25T20:00:00Z'); // 13:00 PDT assert.equal(new Date(nextQuotaReset(noon)).toISOString(), '2026-10-26T07:00:00.000Z'); const justBefore = new Date('2026-10-26T06:59:59Z'); // 23:59:59 PDT assert.equal(new Date(nextQuotaReset(justBefore)).toISOString(), '2026-10-26T07:00:00.000Z'); const exactly = new Date('2026-10-26T07:00:00Z'); // 00:00:00 PDT assert.equal(new Date(nextQuotaReset(exactly)).toISOString(), '2026-10-27T07:00:00.000Z'); }); test('nextQuotaReset points at the next Pacific midnight (winter time)', () => { const noon = new Date('2026-12-15T12:00:00Z'); // 04:00 PST assert.equal(new Date(nextQuotaReset(noon)).toISOString(), '2026-12-16T08:00:00.000Z'); });