Files
wehub-resource-sync f1ba9c6c36
/ test (push) Failing after 1s
/ build-and-push-to-ghcr (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:23:39 +08:00

232 lines
9.8 KiB
TypeScript

/**
* E2E tests for the compression middleware (src/lib/koa-compress.ts).
*
* Covers:
* - Accept-Encoding negotiation across gzip / br / zstd
* - Preference ordering (zstd > br > gzip > deflate) and q-value tie-breaking
* - RFC 9110 identity semantics: strict-prefer identity → no compression
* - X-Decompressed-Content-Length matches the actual decompressed byte length
* - Cache-Control: no-transform is appended on compressed AND on
* deliberately-uncompressed responses (preserves server intent through caches)
* - Vary: Accept-Encoding is always advertised
* - Below-threshold static assets bypass compression
*
* Uses supertest with a custom raw parser so res.body is a Buffer rather than
* a JSON-parsed object. Note: superagent auto-decompresses gzip/br/deflate before
* the custom parser sees the stream — for zstd we decompress manually.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert';
import zlib from 'zlib';
import { promisify } from 'util';
import supertest from 'supertest';
import server from '../../build/stand-alone/crawl.js';
import { SAMPLE_HTML } from '../helpers/client';
function rawParser(res: any, cb: (err: Error | null, body: Buffer) => void) {
const chunks: Buffer[] = [];
res.on('data', (c: Buffer) => chunks.push(c));
res.on('end', () => cb(null, Buffer.concat(chunks)));
res.on('error', (e: Error) => cb(e, Buffer.alloc(0)));
}
function agent() {
return supertest(server.httpServer);
}
async function postCrawl(acceptEncoding: string | undefined) {
let req = agent()
.post('/')
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.buffer(true)
.parse(rawParser);
if (acceptEncoding !== undefined) req = req.set('Accept-Encoding', acceptEncoding);
return req.send({ html: SAMPLE_HTML, url: 'https://example.com/test' });
}
const zstdDecompress = promisify(zlib.zstdDecompress);
// supertest/superagent auto-decompresses gzip/br/deflate before the parser sees
// the stream; we only need to manually decode zstd.
async function decodedPayload(res: supertest.Response): Promise<Buffer> {
const enc = res.headers['content-encoding'];
const body = res.body as Buffer;
if (enc === 'zstd') return zstdDecompress(body);
return body;
}
function cacheControlIncludes(res: supertest.Response, directive: string): boolean {
const cc = String(res.headers['cache-control'] ?? '').toLowerCase();
return cc.split(',').map((s) => s.trim()).includes(directive);
}
describe('compression: encoding negotiation', () => {
it('Accept-Encoding: gzip → gzip, body is valid JSON', async () => {
const res = await postCrawl('gzip');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.headers['content-encoding'], 'gzip');
const json = JSON.parse((await decodedPayload(res)).toString('utf-8'));
assert.ok(json?.data?.content, 'decompressed payload must contain data.content');
});
it('Accept-Encoding: br → br, body is valid JSON', async () => {
const res = await postCrawl('br');
assert.strictEqual(res.headers['content-encoding'], 'br');
const json = JSON.parse((await decodedPayload(res)).toString('utf-8'));
assert.ok(json?.data?.content);
});
it('Accept-Encoding: zstd → zstd, body is valid JSON', async () => {
const res = await postCrawl('zstd');
assert.strictEqual(res.headers['content-encoding'], 'zstd');
const json = JSON.parse((await decodedPayload(res)).toString('utf-8'));
assert.ok(json?.data?.content);
});
it('Accept-Encoding: gzip, br, zstd → picks zstd (top of preference)', async () => {
const res = await postCrawl('gzip, br, zstd');
assert.strictEqual(res.headers['content-encoding'], 'zstd');
});
it('Accept-Encoding: * → picks zstd (top of preference)', async () => {
const res = await postCrawl('*');
assert.strictEqual(res.headers['content-encoding'], 'zstd');
});
it('Accept-Encoding: gzip;q=1, br;q=0.5 → q dominates preference', async () => {
const res = await postCrawl('gzip;q=1, br;q=0.5');
assert.strictEqual(res.headers['content-encoding'], 'gzip');
});
it('Accept-Encoding: identity → no compression', async () => {
const res = await postCrawl('identity');
assert.strictEqual(res.headers['content-encoding'], undefined);
});
it('No Accept-Encoding header → no compression', async () => {
const res = await postCrawl('');
assert.strictEqual(res.headers['content-encoding'], undefined);
});
it('identity, gzip;q=0.5 → identity strictly preferred, no compression', async () => {
const res = await postCrawl('identity, gzip;q=0.5');
assert.strictEqual(res.headers['content-encoding'], undefined);
});
it('gzip, identity;q=0 → identity excluded, gzip wins', async () => {
const res = await postCrawl('gzip, identity;q=0');
assert.strictEqual(res.headers['content-encoding'], 'gzip');
});
it('identity, gzip at equal q → gzip wins (tie ≠ strict-prefer)', async () => {
const res = await postCrawl('identity, gzip');
assert.strictEqual(res.headers['content-encoding'], 'gzip');
});
it('Unknown token (lzma) → no compression', async () => {
const res = await postCrawl('lzma');
assert.strictEqual(res.headers['content-encoding'], undefined);
});
});
describe('compression: response framing', () => {
it('Vary: Accept-Encoding is set on compressed responses', async () => {
const res = await postCrawl('gzip');
const vary = String(res.headers['vary'] ?? '').toLowerCase();
assert.ok(vary.includes('accept-encoding') || vary.includes('*'), `expected Vary to include Accept-Encoding or *, got "${vary}"`);
});
it('Vary: Accept-Encoding is set even when no compression is applied', async () => {
const res = await postCrawl('');
const vary = String(res.headers['vary'] ?? '').toLowerCase();
assert.ok(vary.includes('accept-encoding') || vary.includes('*'), `expected Vary to include Accept-Encoding or *, got "${vary}"`);
});
it('Content-Encoding is a single token (no stacked encodings)', async () => {
const res = await postCrawl('gzip');
const ce = res.headers['content-encoding'];
assert.ok(typeof ce === 'string');
assert.ok(!ce.includes(','), `Content-Encoding should be a single token, got "${ce}"`);
});
it('uncompressed response still carries a valid JSON payload', async () => {
const res = await postCrawl('');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.headers['content-encoding'], undefined);
const json = JSON.parse((res.body as Buffer).toString('utf-8'));
assert.ok(json?.data?.content);
});
});
describe('compression: X-Decompressed-Content-Length', () => {
for (const encoding of ['gzip', 'br', 'zstd'] as const) {
it(`${encoding}: header value equals the decompressed byte length`, async () => {
const res = await postCrawl(encoding);
assert.strictEqual(res.headers['content-encoding'], encoding);
const advertised = res.headers['x-decompressed-content-length'];
assert.ok(advertised, `${encoding} response must set X-Decompressed-Content-Length`);
const decoded = await decodedPayload(res);
assert.strictEqual(
parseInt(String(advertised), 10),
decoded.byteLength,
`${encoding}: X-Decompressed-Content-Length must equal decompressed payload length`,
);
});
}
it('uncompressed response does not set X-Decompressed-Content-Length', async () => {
const res = await postCrawl('');
assert.strictEqual(res.headers['content-encoding'], undefined);
assert.strictEqual(res.headers['x-decompressed-content-length'], undefined);
});
});
describe('compression: Cache-Control no-transform', () => {
it('appends no-transform on compressed responses', async () => {
const res = await postCrawl('gzip');
assert.strictEqual(res.headers['content-encoding'], 'gzip');
assert.ok(
cacheControlIncludes(res, 'no-transform'),
`expected Cache-Control to include no-transform, got "${res.headers['cache-control']}"`,
);
});
it('appends no-transform when negotiation declines compression (identity)', async () => {
const res = await postCrawl('identity');
assert.strictEqual(res.headers['content-encoding'], undefined);
assert.ok(
cacheControlIncludes(res, 'no-transform'),
`expected Cache-Control to include no-transform on identity, got "${res.headers['cache-control']}"`,
);
});
it('appends no-transform on the no-Accept-Encoding path', async () => {
const res = await postCrawl('');
assert.strictEqual(res.headers['content-encoding'], undefined);
assert.ok(
cacheControlIncludes(res, 'no-transform'),
`expected Cache-Control to include no-transform when no Accept-Encoding, got "${res.headers['cache-control']}"`,
);
});
});
describe('compression: threshold', () => {
it('below-threshold static asset (robots.txt, ~375B) is not compressed', async () => {
const res = await agent()
.get('/robots.txt')
.set('Accept-Encoding', 'gzip, br, zstd')
.buffer(true)
.parse(rawParser);
assert.strictEqual(res.status, 200);
assert.strictEqual(
res.headers['content-encoding'],
undefined,
'below-threshold responses must not be compressed',
);
assert.ok(res.headers['content-length'], 'asset response should retain Content-Length');
});
});