mirror of
https://github.com/Twigpine/node-explorer.git
synced 2026-11-11 05:01:32 -10:00
The explorer, its docs and its agent files now name explorer.twigpine.com and node / node2 / node3.twigpine.com, and the proxy rules call the nodes by those names. explorer.gitlawb.com forwards pages to the new address; the API proxy, llms.txt, skill.md and the raw docs keep answering there. The Manila node keeps its current address, and the documented node default for ICAPTCHA_URL is unchanged because it is the value the node ships with.
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { fetchRepository, type ApiRepository } from './repository.js';
|
|
|
|
const REPOSITORY: ApiRepository = {
|
|
id: 'repo-1',
|
|
name: 'demo',
|
|
owner_did: 'did:key:z6Owner',
|
|
description: 'A demo repository',
|
|
is_public: true,
|
|
default_branch: 'main',
|
|
clone_url: 'gitlawb://z6Owner/demo',
|
|
star_count: 7,
|
|
created_at: '2026-07-01T00:00:00.000Z',
|
|
updated_at: '2026-07-10T00:00:00.000Z',
|
|
forked_from: null,
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('fetchRepository', () => {
|
|
it('fetches one absolute, encoded upstream URL and returns a valid repository', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(Response.json(REPOSITORY));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await expect(fetchRepository('did:key:z6 Owner', 'demo/name')).resolves.toEqual({
|
|
status: 'ok',
|
|
repository: REPOSITORY,
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(String(fetchMock.mock.calls[0][0])).toBe(
|
|
'https://node.twigpine.com/api/v1/repos/did%3Akey%3Az6%20Owner/demo%2Fname',
|
|
);
|
|
expect(fetchMock.mock.calls[0][1]).toMatchObject({ method: 'GET', cache: 'no-store' });
|
|
});
|
|
|
|
it('returns not_found for a 404 without parsing the response body', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('not json', { status: 404 })));
|
|
|
|
await expect(fetchRepository('z6Owner', 'missing')).resolves.toEqual({ status: 'not_found' });
|
|
});
|
|
|
|
it('rejects invalid JSON and invalid repository shapes', async () => {
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce(new Response('{', { status: 200 }))
|
|
.mockResolvedValueOnce(Response.json({ ...REPOSITORY, star_count: 'many' }));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await expect(fetchRepository('z6Owner', 'demo')).resolves.toEqual({
|
|
status: 'error',
|
|
reason: 'invalid_response',
|
|
});
|
|
await expect(fetchRepository('z6Owner', 'demo')).resolves.toEqual({
|
|
status: 'error',
|
|
reason: 'invalid_response',
|
|
});
|
|
});
|
|
|
|
it('returns an upstream error with the response status', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('unavailable', { status: 503 })));
|
|
|
|
await expect(fetchRepository('z6Owner', 'demo')).resolves.toEqual({
|
|
status: 'error',
|
|
reason: 'upstream',
|
|
upstreamStatus: 503,
|
|
});
|
|
});
|
|
|
|
it('honors a caller abort without waiting for the internal timeout', async () => {
|
|
const fetchMock = vi.fn((_url: URL, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
|
const signal = init?.signal;
|
|
const rejectAbort = () => reject(new DOMException('Aborted', 'AbortError'));
|
|
if (signal?.aborted) rejectAbort();
|
|
else signal?.addEventListener('abort', rejectAbort, { once: true });
|
|
}));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const controller = new AbortController();
|
|
|
|
const result = fetchRepository('z6Owner', 'demo', controller.signal);
|
|
controller.abort();
|
|
|
|
await expect(result).resolves.toEqual({ status: 'error', reason: 'aborted' });
|
|
});
|
|
});
|