Add comprehensive unit tests and repair stale tests that referenced the old (pre-refactor) src/utils/* layout which no longer exists: - s3-range: expand to 25 cases (suffix, clamping, malformed, zero-size, invalid totals, content-range formatting) - s3-object-stream: rewrite against the real interfaces/s3 module; add multi-part ordering, ranges spanning parts, S3/CORS headers, fetch-error propagation - s3-helpers-edge (new): compress heuristics, virtual-host bucket parsing, S3 route detection, client-IP/trustProxy, S3 response headers - s3-auth-edge (new): verifyBodyHash, isS3Request, canonical-query-string encoding/sorting - chunked-storage: rewrite against the real ChunkedStorage class (was importing deleted src/utils/chunked-storage) — chunk split, hashing, compression, size-limit guards, forwarding - zip: fix stale import + add path-traversal/duplicate sanitization, locateZipEntry, empty-name fallback - s3-docker-registry: fix stale src/config import; correct the rate-limit test to assert S3 routes INTENTIONALLY bypass rate limiting - temp-stream (new): streamToTemp hashing, MD5, signature bytes, empty and oversized streams - package.json: add the S3/unit files to test and test:s3 scripts All new unit tests pass when run per-file (the project's documented mode to avoid cross-file mock pollution). s3-sdk.test.ts (live E2E against a running server) is deliberately excluded from test:s3.
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { buildCanonicalQueryString, isS3Request, verifyBodyHash } from '../src/interfaces/s3/auth';
|
|
|
|
/**
|
|
* Edge-case coverage for the exported pure helpers in the SigV4 auth module
|
|
* that the existing s3-auth.test.ts does not exercise directly.
|
|
*/
|
|
describe('verifyBodyHash (body-integrity check)', () => {
|
|
it('returns null when no x-amz-content-sha256 header is present (unsigned allowed)', () => {
|
|
expect(verifyBodyHash('abc123', {})).toBeNull();
|
|
});
|
|
|
|
it('returns null for UNSIGNED-PAYLOAD', () => {
|
|
expect(verifyBodyHash('anything', { 'x-amz-content-sha256': 'UNSIGNED-PAYLOAD' })).toBeNull();
|
|
});
|
|
|
|
it('returns null for STREAMING-* payloads (checked after signature)', () => {
|
|
expect(
|
|
verifyBodyHash('anything', { 'x-amz-content-sha256': 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns BadDigest when the claimed and actual hash differ', () => {
|
|
const result = verifyBodyHash('actual-hash', { 'x-amz-content-sha256': 'claimed-hash' });
|
|
expect(result).not.toBeNull();
|
|
expect(result?.isValid).toBe(false);
|
|
expect(result?.errorCode).toBe('BadDigest');
|
|
});
|
|
|
|
it('returns null when the claimed and actual hash match', () => {
|
|
expect(verifyBodyHash('same', { 'x-amz-content-sha256': 'same' })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('isS3Request (Auth header detection)', () => {
|
|
it('detects AWS4-HMAC-SHA256 authorization', () => {
|
|
expect(isS3Request({ authorization: 'AWS4-HMAC-SHA256 Credential=...' })).toBe(true);
|
|
});
|
|
|
|
it('returns false for bearer/simple auth', () => {
|
|
expect(isS3Request({ authorization: 'Bearer token' })).toBe(false);
|
|
});
|
|
|
|
it('returns false when no authorization header', () => {
|
|
expect(isS3Request({})).toBe(false);
|
|
});
|
|
|
|
it('is case-sensitive on the AWS4-HMAC-SHA256 scheme prefix', () => {
|
|
expect(isS3Request({ authorization: 'aws4-hmac-sha256 Credential=...' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildCanonicalQueryString (SigV4 query canonicalization)', () => {
|
|
it('returns empty string for no params', () => {
|
|
expect(buildCanonicalQueryString(new URLSearchParams())).toBe('');
|
|
});
|
|
|
|
it('sorts params by encoded key then value (byte order)', () => {
|
|
const sp = new URLSearchParams('b=2&a=1&c=3');
|
|
expect(buildCanonicalQueryString(sp)).toBe('a=1&b=2&c=3');
|
|
});
|
|
|
|
it('sorts by encoded (key=value) pair, not raw key', () => {
|
|
const sp = new URLSearchParams({ 'list-type': '2', prefix: 'x' });
|
|
// 'list-type' (l...) sorts before 'prefix' (p...)
|
|
expect(buildCanonicalQueryString(sp)).toBe('list-type=2&prefix=x');
|
|
});
|
|
|
|
it('excludes the X-Amz-Signature key when requested', () => {
|
|
const sp = new URLSearchParams({
|
|
'X-Amz-Signature': 'sig',
|
|
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
|
});
|
|
const result = buildCanonicalQueryString(sp, new Set(['X-Amz-Signature']));
|
|
expect(result).not.toContain('X-Amz-Signature');
|
|
expect(result).toContain('X-Amz-Algorithm');
|
|
});
|
|
|
|
it('percent-encodes special characters', () => {
|
|
const sp = new URLSearchParams();
|
|
sp.set('key with space', 'a&b');
|
|
const result = buildCanonicalQueryString(sp);
|
|
// space → %20, & → %26
|
|
expect(result).toContain('%20');
|
|
expect(result).toContain('%26');
|
|
});
|
|
});
|