mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
176 lines
7.3 KiB
TypeScript
176 lines
7.3 KiB
TypeScript
import { decideRange, isNotModified, validatorsFor, type Validators } from '../../../../src/nest/storage/http-serving';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
// The pure half of the remote-serving parity work (task C4): every decision a
|
|
// proxying byte-path makes BEFORE it touches a driver or a socket. Kept
|
|
// dependency-free so the state machine is provable in isolation — the
|
|
// service-level suite then only has to prove the wiring.
|
|
|
|
const LM_5000 = new Date(5000).toUTCString();
|
|
|
|
describe('validatorsFor', () => {
|
|
it('prefers the driver-supplied ETag verbatim', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 10, mtimeMs: 5000, etag: '"abc123"' })).toEqual({
|
|
etag: '"abc123"',
|
|
lastModified: LM_5000,
|
|
});
|
|
});
|
|
|
|
it('derives a weak size-mtime ETag when the driver supplies none (send/express parity)', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 26, mtimeMs: 4096 })).toEqual({
|
|
etag: 'W/"1a-1000"',
|
|
lastModified: new Date(4096).toUTCString(),
|
|
});
|
|
});
|
|
|
|
it('emits NO validators at all when mtimeMs is 0 (S3 can report it — never a 1970 header)', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 10, mtimeMs: 0 })).toEqual({});
|
|
});
|
|
|
|
it('still emits a driver ETag when mtimeMs is 0, but no Last-Modified', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 10, mtimeMs: 0, etag: '"abc123"' })).toEqual({
|
|
etag: '"abc123"',
|
|
});
|
|
});
|
|
|
|
it('ignores an empty driver ETag and falls back to the derived one', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 1, mtimeMs: 16, etag: '' }).etag).toBe('W/"1-10"');
|
|
});
|
|
|
|
it('floors a fractional mtime into the derived tag', () => {
|
|
expect(validatorsFor({ key: 'a.bin', size: 1, mtimeMs: 16.9 }).etag).toBe('W/"1-10"');
|
|
});
|
|
});
|
|
|
|
describe('isNotModified', () => {
|
|
const v: Validators = { etag: '"abc"', lastModified: LM_5000 };
|
|
|
|
it('matches If-None-Match exactly', () => {
|
|
expect(isNotModified({ 'if-none-match': '"abc"' }, v)).toBe(true);
|
|
expect(isNotModified({ 'if-none-match': '"other"' }, v)).toBe(false);
|
|
});
|
|
|
|
it('matches a list of candidate tags', () => {
|
|
expect(isNotModified({ 'if-none-match': '"x", "abc" ,"y"' }, v)).toBe(true);
|
|
});
|
|
|
|
it('compares weakly — W/ prefixes on either side are stripped', () => {
|
|
expect(isNotModified({ 'if-none-match': 'W/"abc"' }, v)).toBe(true);
|
|
expect(isNotModified({ 'if-none-match': '"1a-1000"' }, { etag: 'W/"1a-1000"' })).toBe(true);
|
|
});
|
|
|
|
it('honours the * wildcard', () => {
|
|
expect(isNotModified({ 'if-none-match': '*' }, v)).toBe(true);
|
|
expect(isNotModified({ 'if-none-match': '*' }, {})).toBe(true);
|
|
});
|
|
|
|
it('never matches when we have no ETag to compare against', () => {
|
|
expect(isNotModified({ 'if-none-match': '"abc"' }, {})).toBe(false);
|
|
});
|
|
|
|
it('lets If-None-Match win over If-Modified-Since even when the date says fresh', () => {
|
|
expect(isNotModified({ 'if-none-match': '"stale"', 'if-modified-since': LM_5000 }, v)).toBe(false);
|
|
});
|
|
|
|
it('matches If-Modified-Since at second granularity', () => {
|
|
// 5400ms truncates to the same second as 5000ms — a 1970-second-boundary
|
|
// stand-in for the sub-second mtimes Last-Modified cannot express.
|
|
expect(isNotModified({ 'if-modified-since': LM_5000 }, { lastModified: new Date(5400).toUTCString() })).toBe(true);
|
|
expect(isNotModified({ 'if-modified-since': LM_5000 }, { lastModified: new Date(6000).toUTCString() })).toBe(false);
|
|
});
|
|
|
|
it('treats an older If-Modified-Since as modified', () => {
|
|
expect(isNotModified({ 'if-modified-since': new Date(4000).toUTCString() }, v)).toBe(false);
|
|
});
|
|
|
|
it('ignores an unparseable If-Modified-Since', () => {
|
|
expect(isNotModified({ 'if-modified-since': 'not-a-date' }, v)).toBe(false);
|
|
});
|
|
|
|
it('is false when there is no Last-Modified to compare (mtimeMs 0)', () => {
|
|
expect(isNotModified({ 'if-modified-since': LM_5000 }, {})).toBe(false);
|
|
});
|
|
|
|
it('is false with no conditional headers at all', () => {
|
|
expect(isNotModified({}, v)).toBe(false);
|
|
});
|
|
|
|
it('reads the first value of a repeated header', () => {
|
|
expect(isNotModified({ 'if-none-match': ['"abc"', '"zzz"'] }, v)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('decideRange', () => {
|
|
it('is full with no Range header', () => {
|
|
expect(decideRange({}, 10, '"abc"')).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('resolves a closed range inclusively', () => {
|
|
expect(decideRange({ range: 'bytes=2-5' }, 10, '"abc"')).toEqual({ kind: 'partial', start: 2, end: 5 });
|
|
});
|
|
|
|
it('resolves an open-ended range to the last byte', () => {
|
|
expect(decideRange({ range: 'bytes=5-' }, 10, '"abc"')).toEqual({ kind: 'partial', start: 5, end: 9 });
|
|
});
|
|
|
|
it('clamps an end past EOF', () => {
|
|
expect(decideRange({ range: 'bytes=5-999' }, 10, '"abc"')).toEqual({ kind: 'partial', start: 5, end: 9 });
|
|
});
|
|
|
|
it('computes a suffix range from the pre-stat size', () => {
|
|
expect(decideRange({ range: 'bytes=-4' }, 10, '"abc"')).toEqual({ kind: 'partial', start: 6, end: 9 });
|
|
});
|
|
|
|
it('clamps a suffix larger than the object to the whole object', () => {
|
|
expect(decideRange({ range: 'bytes=-999' }, 10, '"abc"')).toEqual({ kind: 'partial', start: 0, end: 9 });
|
|
});
|
|
|
|
it('is unsatisfiable past EOF, on an inverted range, and on a zero-length suffix', () => {
|
|
expect(decideRange({ range: 'bytes=10-12' }, 10, '"abc"')).toEqual({ kind: 'unsatisfiable' });
|
|
expect(decideRange({ range: 'bytes=5-3' }, 10, '"abc"')).toEqual({ kind: 'unsatisfiable' });
|
|
expect(decideRange({ range: 'bytes=-0' }, 10, '"abc"')).toEqual({ kind: 'unsatisfiable' });
|
|
});
|
|
|
|
it('is unsatisfiable for any range against a zero-byte object', () => {
|
|
expect(decideRange({ range: 'bytes=0-' }, 0, '"abc"')).toEqual({ kind: 'unsatisfiable' });
|
|
expect(decideRange({ range: 'bytes=-1' }, 0, '"abc"')).toEqual({ kind: 'unsatisfiable' });
|
|
});
|
|
|
|
it('falls back to a full 200 for a multi-range request (no multipart/byteranges)', () => {
|
|
expect(decideRange({ range: 'bytes=0-1,4-5' }, 10, '"abc"')).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('falls back to a full 200 for malformed or non-byte units', () => {
|
|
for (const range of ['bytes=-', 'bytes=abc', 'items=0-1', 'bytes 0-1', 'bytes=0-1;x', '']) {
|
|
expect(decideRange({ range }, 10, '"abc"')).toEqual({ kind: 'full' });
|
|
}
|
|
});
|
|
|
|
it('honours a strong-exact If-Range match', () => {
|
|
expect(decideRange({ range: 'bytes=2-5', 'if-range': '"abc"' }, 10, '"abc"')).toEqual({
|
|
kind: 'partial',
|
|
start: 2,
|
|
end: 5,
|
|
});
|
|
});
|
|
|
|
it('serves the full entity when If-Range does not strong-match', () => {
|
|
expect(decideRange({ range: 'bytes=2-5', 'if-range': '"stale"' }, 10, '"abc"')).toEqual({ kind: 'full' });
|
|
// A weak validator can never satisfy If-Range (RFC 9110 §13.1.5).
|
|
expect(decideRange({ range: 'bytes=2-5', 'if-range': 'W/"abc"' }, 10, 'W/"abc"')).toEqual({ kind: 'full' });
|
|
// Neither can an HTTP-date form — we only carry the ETag here.
|
|
expect(decideRange({ range: 'bytes=2-5', 'if-range': LM_5000 }, 10, '"abc"')).toEqual({ kind: 'full' });
|
|
// …nor an If-Range sent for a resource we have no ETag for.
|
|
expect(decideRange({ range: 'bytes=2-5', 'if-range': '"abc"' }, 10, undefined)).toEqual({ kind: 'full' });
|
|
});
|
|
|
|
it('reads the first value of a repeated Range header', () => {
|
|
expect(decideRange({ range: ['bytes=2-5', 'bytes=0-1'] }, 10, '"abc"')).toEqual({
|
|
kind: 'partial',
|
|
start: 2,
|
|
end: 5,
|
|
});
|
|
});
|
|
});
|