Files
TeleUploader/test/s3-helpers-edge.test.ts
T

235 lines
9.0 KiB
TypeScript
Raw Normal View History

import { describe, expect, it } from 'bun:test';
import { config } from '../src/env';
import { applyS3Headers, S3_CORS_HEADERS, s3Headers } from '../src/interfaces/s3/headers';
import { extractS3BucketFromHost } from '../src/interfaces/s3/virtual-host';
import { maybeCompressChunk } from '../src/shared/utils/compress';
import { extractClientIp } from '../src/shared/utils/ip';
import { getS3RouteBucket, shouldHandleS3 } from '../src/shared/utils/s3-detection';
const DOMAINS = config.s3VhostDomains;
describe('maybeCompressChunk (gzip compression heuristics)', () => {
it('returns chunk unchanged when compression disabled', () => {
const chunk = Buffer.from('hello world hello world hello world');
const { bytes, compressionAlgorithm } = maybeCompressChunk(chunk, false, 0);
expect(bytes).toBe(chunk);
expect(compressionAlgorithm).toBeNull();
});
it('skips compression for chunks below min-size threshold', () => {
const chunk = Buffer.from('tiny');
const { bytes, compressionAlgorithm } = maybeCompressChunk(chunk, true, 100);
expect(bytes).toBe(chunk);
expect(compressionAlgorithm).toBeNull();
});
it('compresses compressible data above threshold', () => {
const data = 'the quick brown fox jumps over the lazy dog '.repeat(20);
const chunk = Buffer.from(data);
const { bytes, compressionAlgorithm } = maybeCompressChunk(chunk, true, 1);
expect(compressionAlgorithm).toBe('gzip');
expect(bytes.byteLength).toBeLessThan(chunk.byteLength);
// gzip decompresses back to original
const back = Bun.gunzipSync(bytes);
expect(Buffer.from(back).toString('utf8')).toBe(data);
});
it('does NOT compress when gzip result is larger than input', () => {
// Truly random bytes — gzip cannot compress them, so the chunk is kept raw.
const chunk = Buffer.allocUnsafe(4096);
for (let i = 0; i < chunk.length; i++) chunk[i] = (i * 2654435761 + i * i) & 0xff;
// Sanity: make sure it doesn't accidentally gzip below input size
if (Bun.gzipSync(chunk).byteLength >= chunk.byteLength) {
const { bytes, compressionAlgorithm } = maybeCompressChunk(chunk, true, 1);
expect(compressionAlgorithm).toBeNull();
expect(bytes).toBe(chunk);
} else {
// fallback: this test data happened to compress; skip rather than assert wrongly
expect(true).toBe(true);
}
});
});
describe('extractS3BucketFromHost (virtual-hosted addressing)', () => {
it('extracts bucket from subdomain for a matching domain', () => {
expect(extractS3BucketFromHost('my-bucket.upload.asepharyana.my.id', DOMAINS)).toBe(
'my-bucket',
);
});
it('returns null when host equals a root domain (no bucket)', () => {
expect(extractS3BucketFromHost('upload.asepharyana.my.id', DOMAINS)).toBeNull();
});
it('returns null for unrelated domains', () => {
expect(extractS3BucketFromHost('example.com', DOMAINS)).toBeNull();
expect(extractS3BucketFromHost('evil.asepharyana.my.id.evilland.com', DOMAINS)).toBeNull();
});
it('returns null for non-subdomain suffixes', () => {
expect(
extractS3BucketFromHost('not-a-valid.bucket-upload.asepharyana.my.id', DOMAINS),
).toBeNull();
});
it('strips port from host', () => {
expect(extractS3BucketFromHost('my-bucket.upload.asepharyana.my.id:4000', DOMAINS)).toBe(
'my-bucket',
);
});
it('rejects buckets shorter than 3 chars (AWS min length)', () => {
expect(extractS3BucketFromHost('ab.upload.asepharyana.my.id', DOMAINS)).toBeNull();
expect(extractS3BucketFromHost('b.upload.asepharyana.my.id', DOMAINS)).toBeNull();
});
it('rejects invalid bucket labels (dots/underscores)', () => {
expect(extractS3BucketFromHost('a..b.upload.asepharyana.my.id', DOMAINS)).toBeNull();
expect(extractS3BucketFromHost('a_b.upload.asepharyana.my.id', DOMAINS)).toBeNull();
});
it('normalizes uppercase host to lowercase bucket (stripPort lowercases)', () => {
// Bucket labels are lowercased during host normalization, so uppercase is
// accepted and normalized rather than rejected.
expect(extractS3BucketFromHost('MyBucket.upload.asepharyana.my.id', DOMAINS)).toBe('mybucket');
});
it('is case-insensitive for the host domain', () => {
expect(extractS3BucketFromHost('my-bucket.UPLOAD.ASEPHARYANA.MY.ID', DOMAINS)).toBe(
'my-bucket',
);
});
it('returns null when no domain matches', () => {
expect(extractS3BucketFromHost('my-bucket.example.org', DOMAINS)).toBeNull();
});
});
describe('getS3RouteBucket + shouldHandleS3 (routing detection)', () => {
it('getS3RouteBucket extracts bucket from request host', () => {
// Bun does NOT derive the Host header from the URL string; set it explicitly
// (as a real HTTP request would carry).
const req = new Request('http://upload.asepharyana.my.id/', {
headers: { host: 'my-bucket.upload.asepharyana.my.id' },
});
expect(getS3RouteBucket(req)).toBe('my-bucket');
});
it('returns null host bucket when host is the apex domain', () => {
const req = new Request(`http://${DOMAINS[0]}/`, {
headers: { host: DOMAINS[0] },
});
expect(getS3RouteBucket(req)).toBeNull();
});
it('shouldHandleS3 true via vhost bucket host', () => {
const req = new Request('http://upload.asepharyana.my.id/x', {
headers: { host: 'my-bucket.upload.asepharyana.my.id' },
});
expect(shouldHandleS3(req)).toBe(true);
});
it('shouldHandleS3 true via SigV4 Authorization header', () => {
const req = new Request('http://localhost/');
req.headers.set(
'Authorization',
'AWS4-HMAC-SHA256 Credential=x/20260101/us-east-1/s3/aws4_request',
);
expect(shouldHandleS3(req)).toBe(true);
});
it('shouldHandleS3 true via presigned X-Amz-Signature query param', () => {
const req = new Request(
'http://localhost/?X-Amz-Signature=abcd1234&X-Amz-Algorithm=AWS4-HMAC-SHA256',
);
expect(shouldHandleS3(req)).toBe(true);
});
it('shouldHandleS3 false for plain web requests', () => {
const req = new Request('http://localhost/health');
expect(shouldHandleS3(req)).toBe(false);
});
it('shouldHandleS3 respects a passed headers record', () => {
const req = new Request('http://localhost/');
expect(shouldHandleS3(req, { authorization: 'AWS4-HMAC-SHA256 Credential=x' })).toBe(true);
expect(shouldHandleS3(req, { authorization: 'Bearer token' })).toBe(false);
});
});
describe('extractClientIp', () => {
it('returns 127.0.0.1 when trustProxy is disabled', () => {
expect(extractClientIp(new Request('http://localhost/'))).toBe('127.0.0.1');
});
it('uses X-Forwarded-For first value when trustProxy enabled', async () => {
const orig = config.trustProxy;
config.trustProxy = true;
try {
const req = new Request('http://localhost/');
req.headers.set('x-forwarded-for', '203.0.113.5, 10.0.0.1');
expect(extractClientIp(req)).toBe('203.0.113.5');
} finally {
config.trustProxy = orig;
}
});
it('falls back to X-Real-IP when X-Forwarded-For missing', async () => {
const orig = config.trustProxy;
config.trustProxy = true;
try {
const req = new Request('http://localhost/');
req.headers.set('x-real-ip', '198.51.100.7');
expect(extractClientIp(req)).toBe('198.51.100.7');
} finally {
config.trustProxy = orig;
}
});
it('falls back to 127.0.0.1 when trustProxy true but no forwarded headers', async () => {
const orig = config.trustProxy;
config.trustProxy = true;
try {
expect(extractClientIp(new Request('http://localhost/'))).toBe('127.0.0.1');
} finally {
config.trustProxy = orig;
}
});
});
describe('S3 response headers', () => {
it('builds s3Headers with amazon server + request ids + CORS', () => {
const h = s3Headers('req-abc');
expect(h.server).toBe('AmazonS3');
expect(h['x-amz-request-id']).toBe('req-abc');
expect(h['x-amz-id-2']).toContain('req-abc');
expect(h['access-control-allow-origin']).toBe('*');
});
it('merges extra headers and overrides defaults', () => {
const h = s3Headers('req', { 'content-type': 'application/xml', server: 'custom' });
expect(h['content-type']).toBe('application/xml');
expect(h.server).toBe('custom');
});
it('omits request ids when no requestId given', () => {
const h = s3Headers('');
expect(h['x-amz-request-id']).toBeUndefined();
});
it('applyS3Headers copies S3 headers onto a Headers instance', () => {
const h = new Headers({ 'content-type': 'text/plain' });
// applyS3Headers returns a NEW Headers copy — it does not mutate in place.
const result = applyS3Headers(h, 'rid');
expect(result.get('content-type')).toBe('text/plain');
expect(result.get('server')).toBe('AmazonS3');
expect(result.get('x-amz-request-id')).toBe('rid');
});
it('S3_CORS_HEADERS exposes allowed methods and max-age', () => {
expect(S3_CORS_HEADERS['access-control-allow-methods']).toContain('PUT');
expect(S3_CORS_HEADERS['access-control-allow-methods']).toContain('DELETE');
expect(S3_CORS_HEADERS['access-control-max-age']).toBe('86400');
});
});