281 lines
9.0 KiB
TypeScript
281 lines
9.0 KiB
TypeScript
|
|
import { describe, test, expect } from 'bun:test';
|
|||
|
|
import { ValidationError } from '@shade/core';
|
|||
|
|
import {
|
|||
|
|
declaresThumbnail,
|
|||
|
|
encodeStreamControl,
|
|||
|
|
isAllowedThumbnailMime,
|
|||
|
|
mainStreamIdForThumbnail,
|
|||
|
|
parseStreamControl,
|
|||
|
|
THUMBNAIL_MAX_BYTES,
|
|||
|
|
THUMBNAIL_MIME_ALLOWLIST,
|
|||
|
|
thumbnailStreamIdFor,
|
|||
|
|
validateFileMetadata,
|
|||
|
|
} from '../src/index.js';
|
|||
|
|
import type { StreamFileMetadata, StreamInitMessage } from '../src/index.js';
|
|||
|
|
|
|||
|
|
describe('V3.9 fileMetadata — schema', () => {
|
|||
|
|
test('THUMBNAIL_MAX_BYTES is 64 KiB', () => {
|
|||
|
|
expect(THUMBNAIL_MAX_BYTES).toBe(64 * 1024);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('THUMBNAIL_MIME_ALLOWLIST is jpeg/webp/png only', () => {
|
|||
|
|
expect([...THUMBNAIL_MIME_ALLOWLIST]).toEqual(['image/jpeg', 'image/webp', 'image/png']);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('isAllowedThumbnailMime accepts allowlist + rejects others', () => {
|
|||
|
|
for (const m of THUMBNAIL_MIME_ALLOWLIST) expect(isAllowedThumbnailMime(m)).toBe(true);
|
|||
|
|
expect(isAllowedThumbnailMime('image/svg+xml')).toBe(false);
|
|||
|
|
expect(isAllowedThumbnailMime('image/avif')).toBe(false);
|
|||
|
|
expect(isAllowedThumbnailMime('text/html')).toBe(false);
|
|||
|
|
expect(isAllowedThumbnailMime('')).toBe(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('thumbnailStreamIdFor + mainStreamIdForThumbnail roundtrip', () => {
|
|||
|
|
const main = 'AAAAAAAAAAAAAAAAAAAAAA';
|
|||
|
|
const thumb = thumbnailStreamIdFor(main);
|
|||
|
|
expect(thumb).toBe(`${main}.thumb`);
|
|||
|
|
expect(mainStreamIdForThumbnail(thumb)).toBe(main);
|
|||
|
|
expect(mainStreamIdForThumbnail(main)).toBeNull();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('declaresThumbnail requires hash + mime + bytes + streamId', () => {
|
|||
|
|
expect(declaresThumbnail(undefined)).toBe(false);
|
|||
|
|
expect(declaresThumbnail({})).toBe(false);
|
|||
|
|
expect(declaresThumbnail({ thumbnailHash: 'x' })).toBe(false);
|
|||
|
|
expect(
|
|||
|
|
declaresThumbnail({
|
|||
|
|
thumbnailHash: 'x',
|
|||
|
|
thumbnailMime: 'image/webp',
|
|||
|
|
thumbnailBytes: 100,
|
|||
|
|
}),
|
|||
|
|
).toBe(false);
|
|||
|
|
expect(
|
|||
|
|
declaresThumbnail({
|
|||
|
|
thumbnailHash: 'x',
|
|||
|
|
thumbnailMime: 'image/webp',
|
|||
|
|
thumbnailBytes: 100,
|
|||
|
|
thumbnailStreamId: 'BBBB',
|
|||
|
|
}),
|
|||
|
|
).toBe(true);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('V3.9 validateFileMetadata', () => {
|
|||
|
|
test('accepts empty object', () => {
|
|||
|
|
expect(() => validateFileMetadata({})).not.toThrow();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('accepts a fully-populated metadata', () => {
|
|||
|
|
const meta: StreamFileMetadata = {
|
|||
|
|
filename: 'report.pdf',
|
|||
|
|
mimeType: 'application/pdf',
|
|||
|
|
thumbnailStreamId: 'BB',
|
|||
|
|
thumbnailHash: 'aGFzaA==',
|
|||
|
|
thumbnailMime: 'image/webp',
|
|||
|
|
thumbnailBytes: 12345,
|
|||
|
|
};
|
|||
|
|
expect(() => validateFileMetadata(meta)).not.toThrow();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rejects oversized thumbnailBytes', () => {
|
|||
|
|
expect(() =>
|
|||
|
|
validateFileMetadata({
|
|||
|
|
thumbnailMime: 'image/webp',
|
|||
|
|
thumbnailBytes: THUMBNAIL_MAX_BYTES + 1,
|
|||
|
|
}),
|
|||
|
|
).toThrow(ValidationError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rejects negative or non-integer thumbnailBytes', () => {
|
|||
|
|
expect(() =>
|
|||
|
|
validateFileMetadata({ thumbnailMime: 'image/webp', thumbnailBytes: -1 }),
|
|||
|
|
).toThrow(ValidationError);
|
|||
|
|
expect(() =>
|
|||
|
|
validateFileMetadata({ thumbnailMime: 'image/webp', thumbnailBytes: 1.5 }),
|
|||
|
|
).toThrow(ValidationError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rejects thumbnailMime outside allowlist', () => {
|
|||
|
|
expect(() =>
|
|||
|
|
validateFileMetadata({
|
|||
|
|
thumbnailMime: 'image/svg+xml' as never,
|
|||
|
|
thumbnailBytes: 1024,
|
|||
|
|
}),
|
|||
|
|
).toThrow(ValidationError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rejects thumbnailMime declared without thumbnailBytes', () => {
|
|||
|
|
expect(() =>
|
|||
|
|
validateFileMetadata({ thumbnailMime: 'image/webp' }),
|
|||
|
|
).toThrow(ValidationError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('rejects filename with control characters', () => {
|
|||
|
|
expect(() => validateFileMetadata({ filename: 'foo\nbar' })).toThrow(ValidationError);
|
|||
|
|
expect(() => validateFileMetadata({ filename: 'foo\rbar' })).toThrow(ValidationError);
|
|||
|
|
expect(() => validateFileMetadata({ filename: 'foo |