feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
169
apps/mobile-rn/__tests__/incoming-media-intent.test.ts
Normal file
169
apps/mobile-rn/__tests__/incoming-media-intent.test.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
jest.mock('react-native', () => {
|
||||
const nativeModule = {
|
||||
peekPendingImport: jest.fn(),
|
||||
consumePendingImport: jest.fn(),
|
||||
addListener: jest.fn(),
|
||||
removeListeners: jest.fn(),
|
||||
};
|
||||
let incomingListener: (() => void) | null = null;
|
||||
class NativeEventEmitter {
|
||||
addListener(_eventName: string, listener: () => void): { remove: jest.Mock } {
|
||||
incomingListener = listener;
|
||||
return { remove: jest.fn() };
|
||||
}
|
||||
}
|
||||
return {
|
||||
NativeEventEmitter,
|
||||
NativeModules: { D3ROIncomingMedia: nativeModule },
|
||||
Platform: { OS: 'android' },
|
||||
__incomingMediaTestDoubles: {
|
||||
nativeModule,
|
||||
emitIncoming: () => incomingListener?.(),
|
||||
clearIncoming: () => { incomingListener = null; },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-native-file-access', () => {
|
||||
const fileSystem = {
|
||||
exists: jest.fn(),
|
||||
unlink: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
readFileChunk: jest.fn(),
|
||||
};
|
||||
return {
|
||||
Dirs: { CacheDir: '/data/user/0/com.d3ro.voice/cache' },
|
||||
FileSystem: fileSystem,
|
||||
__incomingMediaFileSystem: fileSystem,
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
consumeIncomingMediaAudio,
|
||||
getPendingIncomingMediaId,
|
||||
onIncomingMediaAvailable,
|
||||
} from '../src/features/import/incoming-media-intent';
|
||||
import { AudioPipelineError } from '../src/features/import/audio-import-types';
|
||||
|
||||
const testDoubles = (jest.requireMock('react-native') as {
|
||||
__incomingMediaTestDoubles: {
|
||||
nativeModule: { peekPendingImport: jest.Mock; consumePendingImport: jest.Mock };
|
||||
emitIncoming: () => void;
|
||||
clearIncoming: () => void;
|
||||
};
|
||||
}).__incomingMediaTestDoubles;
|
||||
const mockFileSystem = (jest.requireMock('react-native-file-access') as {
|
||||
__incomingMediaFileSystem: {
|
||||
exists: jest.Mock; unlink: jest.Mock; stat: jest.Mock; readFileChunk: jest.Mock;
|
||||
};
|
||||
}).__incomingMediaFileSystem;
|
||||
|
||||
const PATH = '/data/user/0/com.d3ro.voice/cache/d3ro-incoming-media/incoming-123e4567-e89b-42d3-a456-426614174000.m4a';
|
||||
|
||||
function wavHeader(): string {
|
||||
return Buffer.from('RIFF\u0001\u0002\u0003\u0004WAVE', 'binary').toString('base64');
|
||||
}
|
||||
|
||||
describe('incoming Android media intent bridge', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
testDoubles.clearIncoming();
|
||||
testDoubles.nativeModule.peekPendingImport.mockResolvedValue(null);
|
||||
testDoubles.nativeModule.consumePendingImport.mockResolvedValue(null);
|
||||
mockFileSystem.exists.mockResolvedValue(false);
|
||||
mockFileSystem.unlink.mockResolvedValue(undefined);
|
||||
mockFileSystem.stat.mockResolvedValue({ type: 'file', size: 12 });
|
||||
mockFileSystem.readFileChunk.mockResolvedValue(wavHeader());
|
||||
});
|
||||
|
||||
test('routes a valid app-owned Korean filename through the normal audio validation contract', async () => {
|
||||
testDoubles.nativeModule.consumePendingImport.mockResolvedValue({
|
||||
kind: 'media',
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
path: PATH,
|
||||
uri: `file://${PATH}`,
|
||||
fileName: '회의 녹음 원본.m4a',
|
||||
mimeType: 'audio/wav',
|
||||
sizeBytes: 12,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
|
||||
const input = await consumeIncomingMediaAudio();
|
||||
|
||||
expect(input).not.toBeNull();
|
||||
expect(input?.source).toBe('share-intent');
|
||||
expect(input?.fileName).toBe('회의-녹음-원본.m4a');
|
||||
expect(mockFileSystem.stat).toHaveBeenCalledWith(PATH);
|
||||
});
|
||||
|
||||
test('refuses a malformed path before it can be read or deleted', async () => {
|
||||
testDoubles.nativeModule.consumePendingImport.mockResolvedValue({
|
||||
kind: 'media',
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
path: '/sdcard/Download/voice.m4a',
|
||||
uri: 'file:///sdcard/Download/voice.m4a',
|
||||
fileName: 'voice.m4a',
|
||||
mimeType: 'audio/mp4',
|
||||
sizeBytes: 12,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
|
||||
await expect(consumeIncomingMediaAudio()).rejects.toBeInstanceOf(AudioPipelineError);
|
||||
expect(mockFileSystem.stat).not.toHaveBeenCalled();
|
||||
expect(mockFileSystem.unlink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('uses a one-way native pending id and subscribes to warm deliveries', async () => {
|
||||
testDoubles.nativeModule.peekPendingImport.mockResolvedValue('123e4567-e89b-42d3-a456-426614174000');
|
||||
await expect(getPendingIncomingMediaId()).resolves.toBe('123e4567-e89b-42d3-a456-426614174000');
|
||||
|
||||
const listener = jest.fn();
|
||||
const subscription = onIncomingMediaAvailable(listener);
|
||||
testDoubles.emitIncoming();
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
subscription.remove();
|
||||
});
|
||||
|
||||
test('fails closed when a native pending id has the wrong shape', async () => {
|
||||
testDoubles.nativeModule.peekPendingImport.mockResolvedValue('../../replay');
|
||||
await expect(getPendingIncomingMediaId()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['provider-unavailable', 'file-read', 'revoked access'],
|
||||
['video-no-audio', 'unsupported-mime', 'does not contain an audio track'],
|
||||
['video-audio-unsupported', 'unsupported-mime', 'format is not supported'],
|
||||
['file-too-large', 'file-too-large', 'too large'],
|
||||
])('turns native one-shot failure %s into a visible public pipeline error', async (
|
||||
errorCode,
|
||||
expectedCode,
|
||||
message,
|
||||
) => {
|
||||
testDoubles.nativeModule.consumePendingImport.mockResolvedValue({
|
||||
kind: 'error',
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
errorCode,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
|
||||
await expect(consumeIncomingMediaAudio()).rejects.toMatchObject({
|
||||
code: expectedCode,
|
||||
message: expect.stringContaining(message),
|
||||
});
|
||||
expect(mockFileSystem.stat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects unknown native failure codes instead of exposing native details', async () => {
|
||||
testDoubles.nativeModule.consumePendingImport.mockResolvedValue({
|
||||
kind: 'error',
|
||||
id: '123e4567-e89b-42d3-a456-426614174000',
|
||||
errorCode: 'java.lang.SecurityException: secret-provider',
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
|
||||
await expect(consumeIncomingMediaAudio()).rejects.toMatchObject({
|
||||
code: 'file-read',
|
||||
message: 'Shared media failure metadata is invalid',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue