Files
dockhand-upstream/tests/docker-stream-core.test.ts
T

131 lines
5.7 KiB
TypeScript

// @ts-expect-error -- bun:test is a runtime built-in with no types installed
import { describe, expect, test } from 'bun:test';
import {
createDockerStreamState,
decodeChunkedDockerBody,
processDockerStreamChunk
} from '../src/lib/server/docker-stream-core';
const HTTP_HEAD = 'HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n';
const HTTP_HEAD_CHUNKED = 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n';
/** Build one 8-byte-framed multiplexed frame: type 1=stdout, 2=stderr. */
function frame(type: number, payload: string): Buffer {
const body = Buffer.from(payload, 'utf-8');
const header = Buffer.alloc(8);
header.writeUInt8(type, 0);
header.writeUInt32BE(body.length, 4);
return Buffer.concat([header, body]);
}
describe('processDockerStreamChunk - raw (TTY) stream', () => {
test('passes raw body through once HTTP headers are stripped', () => {
const st = createDockerStreamState(false);
const out = processDockerStreamChunk(Buffer.from(HTTP_HEAD + 'hello world'), st);
expect(out.join('')).toBe('hello world');
});
test('buffers until the full HTTP header terminator arrives', () => {
const st = createDockerStreamState(false);
// header split across two socket events - nothing emitted until \r\n\r\n seen
expect(processDockerStreamChunk(Buffer.from('HTTP/1.1 200 OK\r\n'), st)).toEqual([]);
const out = processDockerStreamChunk(Buffer.from('\r\nlate'), st);
expect(out.join('')).toBe('late');
});
});
describe('processDockerStreamChunk - multiplexed (non-TTY attach) stream', () => {
test('demuxes stdout and stderr frames', () => {
const st = createDockerStreamState(true);
const data = Buffer.concat([Buffer.from(HTTP_HEAD), frame(1, 'out '), frame(2, 'err')]);
expect(processDockerStreamChunk(data, st).join('')).toBe('out err');
});
test('reassembles a frame split across two socket events', () => {
const st = createDockerStreamState(true);
const f = frame(1, 'hello');
// deliver the header + first 2 payload bytes, then the rest
const first = Buffer.concat([Buffer.from(HTTP_HEAD), f.slice(0, 8 + 2)]);
const second = f.slice(8 + 2);
expect(processDockerStreamChunk(first, st)).toEqual([]); // incomplete frame -> nothing yet
expect(processDockerStreamChunk(second, st).join('')).toBe('hello');
});
test('reassembles a frame whose 8-byte header itself is split', () => {
const st = createDockerStreamState(true);
const f = frame(2, 'x');
const first = Buffer.concat([Buffer.from(HTTP_HEAD), f.slice(0, 4)]); // half the header
const second = f.slice(4);
expect(processDockerStreamChunk(first, st)).toEqual([]);
expect(processDockerStreamChunk(second, st).join('')).toBe('x');
});
test('falls back to raw when the framing is not valid multiplexed (proxy stripped it)', () => {
const st = createDockerStreamState(true);
// a byte-0 stream type of 5 is invalid -> raw fallback, and stays raw after
const bogus = Buffer.concat([Buffer.from(HTTP_HEAD), Buffer.from([5, 0, 0, 0, 0, 0, 0, 1]), Buffer.from('raw')]);
const out = processDockerStreamChunk(bogus, st);
expect(out.join('')).toContain('raw');
expect(st.multiplexed).toBe(false);
// subsequent bytes now pass through raw
expect(processDockerStreamChunk(Buffer.from('more'), st).join('')).toBe('more');
});
test('an oversized frame size (>10MB) trips the raw fallback', () => {
const st = createDockerStreamState(true);
const header = Buffer.alloc(8);
header.writeUInt8(1, 0);
header.writeUInt32BE(20 * 1024 * 1024, 4); // 20 MB - beyond the 10 MB cap
const out = processDockerStreamChunk(Buffer.concat([Buffer.from(HTTP_HEAD), header, Buffer.from('body')]), st);
expect(out.join('')).toContain('body');
expect(st.multiplexed).toBe(false);
});
});
describe('processDockerStreamChunk - chunked AND multiplexed together (non-TTY attach over chunked HTTP)', () => {
test('demuxes frames delivered inside chunked bodies, across a chunk seam', () => {
const st = createDockerStreamState(true);
const f = frame(1, 'hello');
// two frames' bytes split into chunks so a frame boundary straddles a chunk boundary
const payload = Buffer.concat([f, frame(2, 'world')]);
const c1 = payload.slice(0, 6); // mid-first-frame
const c2 = payload.slice(6);
const body =
c1.length.toString(16) + '\r\n' ; // chunk-size line for c1
// first event: HTTP head (chunked) + first chunk header+data, no trailing frame complete
const first = Buffer.concat([Buffer.from(HTTP_HEAD_CHUNKED + body), c1, Buffer.from('\r\n')]);
const secondBody = c2.length.toString(16) + '\r\n';
const second = Buffer.concat([Buffer.from(secondBody), c2, Buffer.from('\r\n')]);
const out = [
...processDockerStreamChunk(first, st),
...processDockerStreamChunk(second, st)
];
expect(out.join('')).toBe('helloworld');
});
});
describe('decodeChunkedDockerBody', () => {
test('decodes a chunked body and drops the framing', () => {
const st = createDockerStreamState(false);
// header says chunked; body: "5\r\nhello\r\n"
const out = processDockerStreamChunk(Buffer.from(HTTP_HEAD_CHUNKED + '5\r\nhello\r\n'), st);
expect(out.join('')).toBe('hello');
});
test('reassembles a chunk whose data is split across two events', () => {
const st = createDockerStreamState(false);
processDockerStreamChunk(Buffer.from(HTTP_HEAD_CHUNKED + '5\r\nhel'), st);
const out = processDockerStreamChunk(Buffer.from('lo\r\n'), st);
expect(out.join('')).toBe('hello');
});
test('the 0-size terminating chunk ends the body', () => {
const st = createDockerStreamState(false);
st.headersStripped = true;
st.isChunked = true;
const out = decodeChunkedDockerBody(Buffer.from('3\r\nabc\r\n0\r\n\r\n'), st);
expect(Buffer.concat(out).toString()).toBe('abc');
expect(st.chunkEnded).toBe(true);
});
});