d3ro-voice/apps/mobile-rn/__tests__/recording-durability.test.ts
2026-08-29 18:33:45 +09:00

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);
});
});