Files
stack/packages/model-proxy/tests/client.test.mjs

15 lines
5.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { ManagementClient } from '../src/client.mjs';
const code=c=>e=>e.code===c;
for(const url of ['https://127.0.0.1:18318','http://example.com:18318','http://localhost:18318','http://127.0.0.1:8317','http://user:[email protected]:18318','http://127.0.0.1:18318/path'])test(`proxy-client-invalid: ${url}`,()=>assert.throws(()=>new ManagementClient({baseUrl:url,key:'p1-management'}),code('proxy-client-invalid')));
test('client GET/PUT v8 only, Authorization header, receipt IDs without values or bodies',async()=>{let keys=['p1-client'];const calls=[],receipts=[];const server=http.createServer(async(q,s)=>{let body='';for await(const b of q)body+=b;calls.push({method:q.method,path:q.url,auth:q.headers.authorization,body});if(q.method==='PUT')keys=JSON.parse(body);s.setHeader('Content-Type','application/json');s.end(JSON.stringify(q.method==='GET'?keys:{status:'ok'}));});await new Promise(r=>server.listen(0,'127.0.0.1',r));try{const c=new ManagementClient({baseUrl:`http://127.0.0.1:${server.address().port}`,key:'p1-management',receipt:r=>receipts.push(r)});assert.deepEqual(await c.getKeys(),keys);await c.putKeys(['p1-new']);assert.deepEqual(await c.getKeys(),['p1-new']);assert.deepEqual(calls.map(x=>x.method),['GET','PUT','GET']);assert.ok(calls.every(x=>x.path==='/v8/management/config/access/api-keys'&&x.auth==='Bearer p1-management'));assert.equal(JSON.stringify(receipts).includes('p1-'),false);assert.equal(receipts.length,3);assert.match(receipts[0].keyIds[0],/^[0-9a-f]{12}$/);}finally{server.closeAllConnections();await new Promise(r=>server.close(r));}});
for(const [name,answer]of [['proxy-client-refused',{status:401,body:'p1-management'}],['proxy-client-response',{status:200,body:'{"bad":"p1-management"}'}],['proxy-client-redirect',{status:302,body:''}]])test(name,async()=>{const receipts=[];const s=http.createServer((q,r)=>{r.writeHead(answer.status,{Location:'http://example.com'});r.end(answer.body);});await new Promise(r=>s.listen(0,'127.0.0.1',r));try{const c=new ManagementClient({baseUrl:`http://127.0.0.1:${s.address().port}`,key:'p1-management',receipt:r=>receipts.push(r)});await assert.rejects(c.getKeys(),code(name));assert.equal(JSON.stringify(receipts).includes('p1-management'),false);}finally{s.closeAllConnections();await new Promise(r=>s.close(r));}});
test('proxy-client-down: transport errors redact details',async()=>{const s=http.createServer();await new Promise(r=>s.listen(0,'127.0.0.1',r));const p=s.address().port;await new Promise(r=>s.close(r));await assert.rejects(new ManagementClient({baseUrl:`http://127.0.0.1:${p}`,key:'p1-management'}).getKeys(),code('proxy-client-down'));});
test('proxy-client-invalid: PUT rejects object key records and empty strings locally',async()=>{const c=new ManagementClient({baseUrl:'http://127.0.0.1:18318',key:'p1-management'});for(const keys of [[{key:'p1-client',models:['some']}],[''],null])assert.throws(()=>c.putKeys(keys),code('proxy-client-invalid'));});
test('proxy-management-key-invalid: missing, symlink and public key file refuse without values',async()=>{const fs=await import('node:fs'),os=await import('node:os'),path=await import('node:path');const {loadManagementClient}=await import('../src/client.mjs');for(const variant of ['missing','public','symlink']){const root=fs.mkdtempSync(path.join(os.tmpdir(),'p1-key-'));fs.mkdirSync(path.join(root,'secrets'));const f=path.join(root,'secrets/proxy-management.key');if(variant==='public')fs.writeFileSync(f,'p1-management',{mode:0o644});if(variant==='symlink'){fs.writeFileSync(path.join(root,'other'),'p1-management',{mode:0o600});fs.symlinkSync(path.join(root,'other'),f);}await assert.rejects(loadManagementClient({configDir:root,proxy:{port:18432}}),e=>e.code==='proxy-management-key-invalid'&&!e.message.includes('p1-management'));}});
test('bus management-key loader supplies a private fixture key without exposing it on the client',async()=>{const fs=await import('node:fs'),os=await import('node:os'),path=await import('node:path');const {loadManagementClient}=await import('../src/client.mjs');const root=fs.mkdtempSync(path.join(os.tmpdir(),'p1-key-'));fs.mkdirSync(path.join(root,'secrets'),{mode:0o700});fs.writeFileSync(path.join(root,'secrets/proxy-management.key'),'p1-management',{mode:0o600});const c=await loadManagementClient({configDir:root,proxy:{port:18432}});assert.deepEqual(Object.keys(c),[]);assert.equal(JSON.stringify(c).includes('p1-management'),false);});
test('proxy-client-response: truncated response remains a redacted named refusal',async()=>{const s=http.createServer((q,r)=>{r.writeHead(200,{'Content-Length':'100000'});r.write('["p1-management"');setTimeout(()=>r.destroy(),10);});await new Promise(r=>s.listen(0,'127.0.0.1',r));try{const c=new ManagementClient({baseUrl:`http://127.0.0.1:${s.address().port}`,key:'p1-management'});await assert.rejects(c.getKeys(),e=>e.code==='proxy-client-response'&&!e.message.includes('p1-management'));}finally{s.closeAllConnections();await new Promise(r=>s.close(r));}});
test('management client ignores ambient fetch dispatchers',async()=>{const original=globalThis.fetch;globalThis.fetch=()=>{throw Error('ambient dispatcher must not see key');};const s=http.createServer((q,r)=>{r.setHeader('Content-Type','application/json');r.end('["p1-client"]');});await new Promise(r=>s.listen(0,'127.0.0.1',r));try{assert.deepEqual(await new ManagementClient({baseUrl:`http://127.0.0.1:${s.address().port}`,key:'p1-management'}).getKeys(),['p1-client']);}finally{globalThis.fetch=original;s.closeAllConnections();await new Promise(r=>s.close(r));}});