2026-07-07 04:11:10 +07:00
|
|
|
import { describe, expect, it } from 'bun:test';
|
2026-08-27 15:48:14 +07:00
|
|
|
import {
|
|
|
|
|
contentRange,
|
|
|
|
|
parseRangeHeader,
|
|
|
|
|
unsatisfiedContentRange,
|
|
|
|
|
} from '../src/interfaces/s3/range';
|
2026-07-07 04:11:10 +07:00
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
/**
|
|
|
|
|
* S3 HTTP Range parser — comprehensive edge-case coverage.
|
|
|
|
|
* Source: src/interfaces/s3/range.ts (the module that s3-controller and
|
|
|
|
|
* object-stream actually use).
|
|
|
|
|
*/
|
2026-07-07 04:11:10 +07:00
|
|
|
describe('S3 HTTP range parser', () => {
|
2026-08-27 15:48:14 +07:00
|
|
|
describe('no range', () => {
|
|
|
|
|
it('returns none when Range header is null', () => {
|
|
|
|
|
expect(parseRangeHeader(null, 10)).toEqual({ type: 'none' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns none when Range header is empty string', () => {
|
|
|
|
|
expect(parseRangeHeader('', 10)).toEqual({ type: 'none' });
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
describe('valid explicit ranges', () => {
|
|
|
|
|
it('parses start/end', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=2-5', 10)).toEqual({ type: 'valid', start: 2, end: 5 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('parses single-byte range', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=3-3', 10)).toEqual({ type: 'valid', start: 3, end: 3 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clamps end beyond object size', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=7-20', 10)).toEqual({ type: 'valid', start: 7, end: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clamps end past last byte for open-ended range', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=7-', 10)).toEqual({ type: 'valid', start: 7, end: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('parses range starting at byte 0', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=0-4', 10)).toEqual({ type: 'valid', start: 0, end: 4 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses last byte for open-ended range with no end', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=9-', 10)).toEqual({ type: 'valid', start: 9, end: 9 });
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
describe('suffix ranges (bytes=-N)', () => {
|
|
|
|
|
it('returns last N bytes', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-4', 10)).toEqual({ type: 'valid', start: 6, end: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns suffix of length equal to full object', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-10', 10)).toEqual({ type: 'valid', start: 0, end: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('clamps oversized suffix to whole object', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-50', 10)).toEqual({ type: 'valid', start: 0, end: 9 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns last 1 byte', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-1', 10)).toEqual({ type: 'valid', start: 9, end: 9 });
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
describe('invalid ranges', () => {
|
|
|
|
|
it('rejects multiple comma-separated ranges', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=0-1,3-4', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects non-bytes units', () => {
|
|
|
|
|
expect(parseRangeHeader('items=0-1', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects start beyond last byte', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=10-12', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects end before start', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=6-3', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects zero-length suffix', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-0', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects non-numeric start', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=aa-5', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects non-numeric end', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=1-bb', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects empty both-sides range', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects negative suffix length', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=-3x', 10)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects range on zero-size object', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=0-0', 0)).toEqual({ type: 'invalid' });
|
|
|
|
|
expect(parseRangeHeader('bytes=-1', 0)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects invalid total size', () => {
|
|
|
|
|
expect(parseRangeHeader('bytes=0-1', -1)).toEqual({ type: 'invalid' });
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
});
|
|
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
describe('header formatting', () => {
|
|
|
|
|
it('formats content-range', () => {
|
|
|
|
|
expect(contentRange(0, 9, 10)).toBe('bytes 0-9/10');
|
|
|
|
|
expect(contentRange(2, 5, 10)).toBe('bytes 2-5/10');
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
|
2026-08-27 15:48:14 +07:00
|
|
|
it('formats unsatisfied content-range', () => {
|
|
|
|
|
expect(unsatisfiedContentRange(10)).toBe('bytes */10');
|
|
|
|
|
expect(unsatisfiedContentRange(0)).toBe('bytes */0');
|
|
|
|
|
});
|
2026-07-07 04:11:10 +07:00
|
|
|
});
|
|
|
|
|
});
|