83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import { SUPABASE_URL } from '@d3ro/core/supabase-config';
|
|
import { resumableAudioUploadTestContract } from '../src/features/import/resumable-audio-upload';
|
|
import {
|
|
durableQueueTestContract,
|
|
type DurableQueueItem,
|
|
} from '../src/features/recording/durable-processing-queue';
|
|
|
|
const USER_ID = '11111111-1111-4111-8111-111111111111';
|
|
|
|
function queueItem(overrides: Partial<DurableQueueItem> = {}): DurableQueueItem {
|
|
const path = `${durableQueueTestContract.queueDirectory}/queued-abc123-def456.wav`;
|
|
return {
|
|
schemaVersion: 1,
|
|
id: 'abc123-def456',
|
|
userId: USER_ID,
|
|
meetingId: null,
|
|
path,
|
|
uri: `file://${path}`,
|
|
fileName: 'recording.wav',
|
|
mimeType: 'audio/wav',
|
|
sizeBytes: 1_024,
|
|
durationMs: 1_000,
|
|
source: 'recording',
|
|
languageCode: 'ko',
|
|
status: 'retry',
|
|
phase: 'uploading',
|
|
attempts: 2,
|
|
uploadedBytes: 512,
|
|
nextAttemptAtMs: 1_000,
|
|
lastErrorCode: 'upload',
|
|
lastErrorMessage: 'offline',
|
|
createdAtMs: 1,
|
|
updatedAtMs: 2,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('resumable audio upload contract', () => {
|
|
test('uses Supabase TUS six-megabyte chunks and UTF-8 metadata', () => {
|
|
expect(resumableAudioUploadTestContract.chunkBytes).toBe(6 * 1024 * 1024);
|
|
expect(resumableAudioUploadTestContract.encodeUtf8Base64('audio')).toBe('YXVkaW8=');
|
|
expect(resumableAudioUploadTestContract.encodeUtf8Base64('음성')).toBe(
|
|
Buffer.from('음성', 'utf8').toString('base64'),
|
|
);
|
|
});
|
|
|
|
test('accepts only the Supabase resumable endpoint and rejects an injected host', () => {
|
|
const origin = new URL(SUPABASE_URL).origin;
|
|
expect(
|
|
resumableAudioUploadTestContract.allowedUploadUrl(
|
|
`${origin}/storage/v1/upload/resumable/session-1`,
|
|
),
|
|
).toContain('/storage/v1/upload/resumable/session-1');
|
|
expect(() => resumableAudioUploadTestContract.allowedUploadUrl(
|
|
'https://attacker.invalid/storage/v1/upload/resumable/session-1',
|
|
)).toThrow('untrusted');
|
|
expect(() => resumableAudioUploadTestContract.allowedUploadUrl(
|
|
`${origin}/rest/v1/private`,
|
|
)).toThrow('untrusted');
|
|
});
|
|
});
|
|
|
|
describe('durable processing queue validation', () => {
|
|
test('keeps a complete owned checkpoint and rejects path traversal', () => {
|
|
const valid = queueItem();
|
|
expect(durableQueueTestContract.parseQueue(JSON.stringify([valid]))).toEqual([valid]);
|
|
expect(durableQueueTestContract.ownedQueuePath(valid.path)).toBe(true);
|
|
expect(durableQueueTestContract.ownedQueuePath(
|
|
`${durableQueueTestContract.queueDirectory}/../account.json`,
|
|
)).toBe(false);
|
|
expect(durableQueueTestContract.parseQueue(JSON.stringify([
|
|
queueItem({ path: '/sdcard/private.wav', uri: 'file:///sdcard/private.wav' }),
|
|
]))).toEqual([]);
|
|
});
|
|
|
|
test('rejects impossible upload offsets and caps exponential retry delay', () => {
|
|
expect(durableQueueTestContract.parseQueue(JSON.stringify([
|
|
queueItem({ uploadedBytes: 2_048 }),
|
|
]))).toEqual([]);
|
|
expect(durableQueueTestContract.retryDelayMs(1)).toBe(5_000);
|
|
expect(durableQueueTestContract.retryDelayMs(99)).toBe(15 * 60_000);
|
|
});
|
|
});
|