Add native local infrastructure integrations
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { IppClient, IppIntegration } from '../../ts/integrations/ipp/index.js';
|
||||
|
||||
tap.test('reads IPP printer attributes with a native Get-Printer-Attributes request', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{ url: string; method?: string; body: Buffer }> = [];
|
||||
globalThis.fetch = (async (urlArg: URL | RequestInfo, initArg?: RequestInit) => {
|
||||
calls.push({ url: String(urlArg), method: initArg?.method, body: Buffer.from(initArg?.body as ArrayBuffer) });
|
||||
return new Response(arrayBuffer(ippResponse([
|
||||
stringAttribute(0x42, 'printer-name', ['TCP Printer']),
|
||||
stringAttribute(0x41, 'printer-info', ['Ready over IPP']),
|
||||
stringAttribute(0x41, 'printer-make-and-model', ['Brother HL-L2350DW']),
|
||||
stringAttribute(0x41, 'printer-device-id', ['MFG:Brother;MDL:HL-L2350DW;CMD:PCL;SERIALNUMBER:BR123;']),
|
||||
stringAttribute(0x45, 'printer-uri-supported', ['ipp://printer.local:631/ipp/print']),
|
||||
enumAttribute('printer-state', [4]),
|
||||
stringAttribute(0x41, 'printer-state-message', ['Printing']),
|
||||
stringAttribute(0x44, 'printer-state-reasons', ['none']),
|
||||
booleanAttribute('printer-is-accepting-jobs', true),
|
||||
integerAttribute('queued-job-count', 1),
|
||||
stringAttribute(0x42, 'marker-names', ['Black Toner']),
|
||||
stringAttribute(0x44, 'marker-types', ['toner-cartridge']),
|
||||
integerAttribute('marker-levels', 73),
|
||||
])), { status: 200, headers: { 'content-type': 'application/ipp' } });
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const snapshot = await new IppClient({ host: 'printer.local', port: 631, basePath: '/ipp/print', timeoutMs: 1000 }).getSnapshot();
|
||||
expect(snapshot.online).toBeTrue();
|
||||
expect(snapshot.printer.manufacturer).toEqual('Brother');
|
||||
expect(snapshot.printer.serialNumber).toEqual('BR123');
|
||||
expect(snapshot.status.printerState).toEqual('printing');
|
||||
expect(snapshot.status.queuedJobCount).toEqual(1);
|
||||
expect(snapshot.markers[0].level).toEqual(73);
|
||||
expect(calls[0].url).toEqual('http://printer.local:631/ipp/print');
|
||||
expect(calls[0].method).toEqual('POST');
|
||||
expect(calls[0].body.readUInt16BE(2)).toEqual(0x000b);
|
||||
expect(calls[0].body.includes(Buffer.from('printer-uri'))).toBeTrue();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('read-only IPP runtime exposes snapshot and refuses unsupported writes', async () => {
|
||||
const runtime = await new IppIntegration().setup({
|
||||
snapshot: {
|
||||
printer: { id: 'snapshot-printer', name: 'Snapshot Printer' },
|
||||
status: { printerState: 'idle', stateReasons: [] },
|
||||
markers: [{ index: 0, name: 'Black Toner', kind: 'toner', level: 55 }],
|
||||
jobs: [],
|
||||
online: true,
|
||||
},
|
||||
}, {});
|
||||
const snapshotResult = await runtime.callService?.({ domain: 'ipp', service: 'snapshot', target: {} });
|
||||
expect(snapshotResult?.success).toBeTrue();
|
||||
const unsupported = await runtime.callService?.({ domain: 'switch', service: 'turn_on', target: {} });
|
||||
expect(unsupported?.success).toBeFalse();
|
||||
expect((await runtime.entities()).find((entityArg) => entityArg.id === 'sensor.black_toner')?.state).toEqual(55);
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
tap.test('refresh reports failure for configs without host, client, attributes, or snapshot', async () => {
|
||||
const runtime = await new IppIntegration().setup({ name: 'No Source Printer' }, {});
|
||||
const result = await runtime.callService?.({ domain: 'ipp', service: 'refresh', target: {} });
|
||||
expect(result?.success).toBeFalse();
|
||||
await runtime.destroy();
|
||||
});
|
||||
|
||||
tap.test('refresh reports failed live IPP reads instead of faking success', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => {
|
||||
throw new Error('connection refused');
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const runtime = await new IppIntegration().setup({ host: 'printer.local', timeoutMs: 1000 }, {});
|
||||
const result = await runtime.callService?.({ domain: 'ipp', service: 'refresh', target: {} });
|
||||
expect(result?.success).toBeFalse();
|
||||
expect(result?.error).toEqual('connection refused');
|
||||
await runtime.destroy();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
const ippResponse = (attributesArg: Buffer[]): Buffer => {
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x01, 0x01]),
|
||||
uint16(0x0000),
|
||||
uint32(1),
|
||||
Buffer.from([0x04]),
|
||||
...attributesArg,
|
||||
Buffer.from([0x03]),
|
||||
]);
|
||||
};
|
||||
|
||||
const stringAttribute = (tagArg: number, nameArg: string, valuesArg: string[]): Buffer => {
|
||||
return Buffer.concat(valuesArg.map((valueArg, indexArg) => value(tagArg, indexArg === 0 ? nameArg : '', Buffer.from(valueArg, 'utf8'))));
|
||||
};
|
||||
|
||||
const integerAttribute = (nameArg: string, valueArg: number): Buffer => {
|
||||
const buffer = Buffer.alloc(4);
|
||||
buffer.writeInt32BE(valueArg, 0);
|
||||
return value(0x21, nameArg, buffer);
|
||||
};
|
||||
|
||||
const enumAttribute = (nameArg: string, valuesArg: number[]): Buffer => {
|
||||
return Buffer.concat(valuesArg.map((valueArg, indexArg) => {
|
||||
const buffer = Buffer.alloc(4);
|
||||
buffer.writeInt32BE(valueArg, 0);
|
||||
return value(0x23, indexArg === 0 ? nameArg : '', buffer);
|
||||
}));
|
||||
};
|
||||
|
||||
const booleanAttribute = (nameArg: string, valueArg: boolean): Buffer => value(0x22, nameArg, Buffer.from([valueArg ? 1 : 0]));
|
||||
|
||||
const value = (tagArg: number, nameArg: string, valueArg: Buffer): Buffer => {
|
||||
const name = Buffer.from(nameArg, 'utf8');
|
||||
return Buffer.concat([Buffer.from([tagArg]), uint16(name.length), name, uint16(valueArg.length), valueArg]);
|
||||
};
|
||||
|
||||
const uint16 = (valueArg: number): Buffer => {
|
||||
const buffer = Buffer.alloc(2);
|
||||
buffer.writeUInt16BE(valueArg, 0);
|
||||
return buffer;
|
||||
};
|
||||
|
||||
const uint32 = (valueArg: number): Buffer => {
|
||||
const buffer = Buffer.alloc(4);
|
||||
buffer.writeUInt32BE(valueArg, 0);
|
||||
return buffer;
|
||||
};
|
||||
|
||||
const arrayBuffer = (bufferArg: Buffer): ArrayBuffer => bufferArg.buffer.slice(bufferArg.byteOffset, bufferArg.byteOffset + bufferArg.byteLength) as ArrayBuffer;
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user