import assert from 'node:assert/strict'; import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { test } from 'node:test'; import { LandingClient } from '../src/client.mjs'; const showPage = { method: 'GET', path: '/v1/pages/{page}', params: [{ name: 'page', in: 'path' }, { name: 'lang', in: 'query' }], hasBody: false, requiresAuth: true, }; const publicProjects = { method: 'GET', path: '/public/v1/projects', params: [{ name: 'featured', in: 'query' }], hasBody: false, requiresAuth: false, }; /** A fake fetch that answers from a list of handlers and records every call. */ function fakeFetch(handler) { const calls = []; const fetch = async (url, init) => { calls.push({ url, ...init }); const { status = 200, body = {} } = handler(url, init, calls.length); return new Response(status === 204 ? null : JSON.stringify(body), { status }); }; return { fetch, calls }; } const tokenResponse = (access, expiresInMs = 3_600_000) => ({ access_token: access, refresh_token: `refresh-${access}`, expires_at: new Date(Date.now() + expiresInMs).toISOString(), }); test('builds the URL from path and query parameters', async () => { const { fetch, calls } = fakeFetch(() => ({ body: { data: [] } })); const client = new LandingClient({ apiBase: 'http://app.test/api/', fetch }); await client.call(publicProjects, { featured: true }); assert.equal(calls[0].url, 'http://app.test/api/public/v1/projects?featured=1'); assert.equal(calls[0].headers.Authorization, undefined); }); test('sends the preview secret on public calls only', async () => { const { fetch, calls } = fakeFetch((url) => ( url.endsWith('/auth/token') ? { body: tokenResponse('a') } : { body: {} } )); const client = new LandingClient({ apiBase: 'http://app.test/api', username: 'u', password: 'p', previewToken: 'secret', fetch, }); await client.call(publicProjects, {}); await client.call(showPage, { page: 1 }); assert.equal(calls[0].headers['X-Preview-Token'], 'secret'); assert.equal(calls.at(-1).headers['X-Preview-Token'], undefined); }); test('logs in once and reuses the token', async () => { const { fetch, calls } = fakeFetch((url) => ( url.endsWith('/auth/token') ? { body: tokenResponse('a') } : { body: { data: {} } } )); const client = new LandingClient({ apiBase: 'http://app.test/api', username: 'u', password: 'p', fetch }); await Promise.all([client.call(showPage, { page: 1 }), client.call(showPage, { page: 2 })]); await client.call(showPage, { page: 3 }); assert.equal(calls.filter((c) => c.url.endsWith('/auth/token')).length, 1); assert.equal(calls.at(-1).url, 'http://app.test/api/v1/pages/3'); assert.equal(calls.at(-1).headers.Authorization, 'Bearer a'); }); test('refreshes a token about to expire', async () => { let issued = 0; const { fetch, calls } = fakeFetch((url) => ( url.endsWith('/auth/token') ? { body: tokenResponse(`t${++issued}`, 10_000) } : { body: {} } )); const client = new LandingClient({ apiBase: 'http://app.test/api', username: 'u', password: 'p', fetch }); await client.call(showPage, { page: 1 }); await client.call(showPage, { page: 1 }); const grants = calls.filter((c) => c.url.endsWith('/auth/token')).map((c) => JSON.parse(c.body).grant_type); assert.deepEqual(grants, ['password', 'refresh_token']); assert.equal(calls.at(-1).headers.Authorization, 'Bearer t2'); }); test('retries once with a new token after a 401', async () => { let issued = 0; const { fetch, calls } = fakeFetch((url, init) => { if (url.endsWith('/auth/token')) return { body: tokenResponse(`t${++issued}`) }; return init.headers.Authorization === 'Bearer t1' ? { status: 401, body: { message: 'Unauthenticated.' } } : { body: { ok: true } }; }); const client = new LandingClient({ apiBase: 'http://app.test/api', username: 'u', password: 'p', fetch }); const response = await client.call(showPage, { page: 1 }); assert.equal(response.status, 200); assert.equal(calls.at(-1).headers.Authorization, 'Bearer t2'); }); test('falls back to the password when the refresh token is refused', async () => { let issued = 0; const { fetch, calls } = fakeFetch((url, init) => { if (!url.endsWith('/auth/token')) return { body: {} }; return JSON.parse(init.body).grant_type === 'refresh_token' ? { status: 422, body: { message: 'invalid' } } : { body: tokenResponse(`t${++issued}`, 10_000) }; }); const client = new LandingClient({ apiBase: 'http://app.test/api', username: 'u', password: 'p', fetch }); await client.call(showPage, { page: 1 }); await client.call(showPage, { page: 1 }); const grants = calls.filter((c) => c.url.endsWith('/auth/token')).map((c) => JSON.parse(c.body).grant_type); assert.deepEqual(grants, ['password', 'refresh_token', 'password']); }); test('a fixed token is used as is and never renewed', async () => { const { fetch, calls } = fakeFetch(() => ({ status: 401, body: {} })); const client = new LandingClient({ apiBase: 'http://app.test/api', token: 'fixed', fetch }); const response = await client.call(showPage, { page: 1 }); assert.equal(response.status, 401); assert.equal(calls.length, 1); assert.equal(calls[0].headers.Authorization, 'Bearer fixed'); }); test('refuses an authenticated call without credentials', async () => { const client = new LandingClient({ apiBase: 'http://app.test/api', fetch: async () => assert.fail('no request') }); await assert.rejects(client.call(showPage, { page: 1 }), /LANDING_TOKEN/); }); test('refuses a call missing a path parameter', async () => { const client = new LandingClient({ apiBase: 'http://app.test/api', token: 't', fetch: async () => assert.fail('no request') }); await assert.rejects(client.call(showPage, {}), /"page"/); }); const uploadMedia = { method: 'POST', path: '/v1/media', params: [], hasBody: true, files: ['file'], requiresAuth: true, }; test('uploads a local file as multipart, never as JSON', async () => { const dir = await mkdtemp(join(tmpdir(), 'landing-mcp-')); const file = join(dir, 'hero.png'); await writeFile(file, Buffer.from([0x89, 0x50, 0x4e, 0x47])); const { fetch, calls } = fakeFetch(() => ({ status: 201, body: { data: { path: 'pages/x.png' } } })); const client = new LandingClient({ apiBase: 'http://app.test/api', token: 't', fetch }); const response = await client.call(uploadMedia, { file_path: file }); const sent = calls[0].body; assert.equal(response.status, 201); assert.ok(sent instanceof FormData); assert.equal(sent.get('file').name, 'hero.png'); assert.equal(sent.get('file').size, 4); // fetch has to write it, boundary and all. assert.equal(calls[0].headers['Content-Type'], undefined); }); test('sends the JSON body when there is no file', async () => { const { fetch, calls } = fakeFetch(() => ({ status: 201, body: {} })); const client = new LandingClient({ apiBase: 'http://app.test/api', token: 't', fetch }); await client.call(uploadMedia, { body: { url: 'https://images.example.org/a.png' } }); assert.equal(calls[0].headers['Content-Type'], 'application/json'); assert.equal(JSON.parse(calls[0].body).url, 'https://images.example.org/a.png'); }); test('says which file it could not read', async () => { const client = new LandingClient({ apiBase: 'http://app.test/api', token: 't', fetch: async () => assert.fail('no request') }); await assert.rejects(client.call(uploadMedia, { file_path: '/nope/missing.png' }), /\/nope\/missing\.png/); });