diff --git a/jest.config.js b/jest.config.js index 69cd14f..7cbaf7f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,10 @@ module.exports = { setupFilesAfterEnv: ['/tests/setup.ts'], moduleNameMapper: { '^@/(.*)$': '/src/$1', - '^pkg/(.*)$': '/pkg/$1' + '^~/(.*)$': '/src/$1', + '^pkg/(.*)$': '/pkg/$1', + '^(.*)\\?raw$': '/tests/rawFileMock.js', + '\\.(css|less|sass|scss)$': '/tests/styleMock.js' }, testTimeout: 10000, transform: { diff --git a/tests/rawFileMock.js b/tests/rawFileMock.js new file mode 100644 index 0000000..9dc5fc1 --- /dev/null +++ b/tests/rawFileMock.js @@ -0,0 +1 @@ +module.exports = ''; diff --git a/tests/styleMock.js b/tests/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/tests/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/tests/unit/channel-messages.test.ts b/tests/unit/channel-messages.test.ts new file mode 100644 index 0000000..503ecf1 --- /dev/null +++ b/tests/unit/channel-messages.test.ts @@ -0,0 +1,155 @@ +import { registerAllListeners } from '../../src/router'; +import { MessageType } from '../../src/models/process.model'; + +// Mocks TokenService +jest.mock('../../src/services/token', () => { + class TokenServiceMock { + static async getInstance() { return new TokenServiceMock(); } + async generateSessionToken(origin: string) { + return { accessToken: `access-${origin}`, refreshToken: `refresh-${origin}` }; + } + async validateToken(token: string, origin: string) { return true; } + async refreshAccessToken(refreshToken: string, origin: string) { return `new-access-${origin}`; } + } + return { __esModule: true, default: TokenServiceMock }; +}); + +// Mocks Services +jest.mock('../../src/services/service', () => { + class ServicesMock { + static instance: any; + static async getInstance() { + if (!ServicesMock.instance) ServicesMock.instance = new ServicesMock(); + return ServicesMock.instance; + } + isPaired() { return true; } + async confirmPairing() { /* no-op */ } + getPairingProcessId() { return 'pair-123'; } + async getProcesses() { return { 'p1': { id: 'p1' } as any }; } + async getMyProcesses() { return ['p1', 'p2']; } + async getProcess(id: string) { return { states: [{ state_id: 'a'.repeat(64), public_data: {}, pcd_commitment: {}, validation_tokens: [] }] } as any; } + async decryptAttribute() { return null; } + decodeValue(v: any) { return v; } + getHashForFile() { return 'hash-abc'; } + getMerkleProofForFile() { return { proof: true } as any; } + validateMerkleProof() { return true; } + async createPrdUpdate() { return {}; } + async approveChange() { return { updated_process: { id: 'p1' } }; } + async updateProcess() { return { updated_process: { id: 'p1' } }; } + async createProcess() { return { updated_process: { process_id: 'pid', current_process: { states: [{ state_id: 'a'.repeat(64) }] } } } as any; } + async handleApiReturn() { /* no-op */ } + } + return { __esModule: true, default: ServicesMock }; +}); + +// Mocks ModalService (used by REQUEST_LINK) +jest.mock('../../src/services/modal.service', () => { + class ModalServiceMock { + static async getInstance() { return new ModalServiceMock(); } + async showConfirmationModal() { return true; } + } + return { __esModule: true, default: ModalServiceMock }; +}); + +describe('postMessage interfaces (registerAllListeners)', () => { + const ORIGIN = 'https://host.test'; + let postSpy: jest.SpyInstance; + + beforeAll(async () => { + postSpy = jest.spyOn(window.parent, 'postMessage'); + await registerAllListeners(); + }); + + afterEach(() => { + postSpy.mockClear(); + }); + + afterAll(() => { + postSpy.mockRestore(); + }); + + function dispatch(data: any) { + window.dispatchEvent(new MessageEvent('message', { data, origin: ORIGIN })); + } + + async function waitForLastCall(expectedType?: string, prevCount?: number) { + const start = Date.now(); + const timeoutMs = 1000; + // eslint-disable-next-line no-constant-condition + while (true) { + const calls = postSpy.mock.calls; + const countOk = prevCount === undefined ? calls.length > 0 : calls.length > (prevCount || 0); + if (countOk) { + const last = calls[calls.length - 1] as any; + const payload = last && last[0]; + if (!expectedType || (payload && payload.type === expectedType)) { + return last; + } + } + if (Date.now() - start > timeoutMs) throw new Error('Timeout waiting for postMessage'); + await new Promise(r => setTimeout(r, 10)); + } + } + + it('REQUEST_LINK -> LINK_ACCEPTED avec tokens', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.REQUEST_LINK, messageId: 'm1' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.LINK_ACCEPTED, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.LINK_ACCEPTED); + expect(payload.accessToken).toContain('access-'); + expect(payload.refreshToken).toContain('refresh-'); + expect(payload.messageId).toBe('m1'); + }); + + it('VALIDATE_TOKEN -> VALIDATE_TOKEN avec isValid=true', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.VALIDATE_TOKEN, accessToken: 'a', refreshToken: 'r', messageId: 'm2' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.VALIDATE_TOKEN, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.VALIDATE_TOKEN); + expect(payload.isValid).toBe(true); + expect(payload.messageId).toBe('m2'); + }); + + it('RENEW_TOKEN -> RENEW_TOKEN avec nouveau accessToken', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.RENEW_TOKEN, refreshToken: 'r', messageId: 'm3' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.RENEW_TOKEN, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.RENEW_TOKEN); + expect(typeof payload.accessToken).toBe('string'); + expect(payload.refreshToken).toBe('r'); + expect(payload.messageId).toBe('m3'); + }); + + it('GET_PAIRING_ID -> GET_PAIRING_ID avec userPairingId', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.GET_PAIRING_ID, accessToken: 'a', messageId: 'm4' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.GET_PAIRING_ID, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.GET_PAIRING_ID); + expect(payload.userPairingId).toBe('pair-123'); + expect(payload.messageId).toBe('m4'); + }); + + it('GET_MY_PROCESSES -> GET_MY_PROCESSES avec liste', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.GET_MY_PROCESSES, accessToken: 'a', messageId: 'm5' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.GET_MY_PROCESSES, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.GET_MY_PROCESSES); + expect(Array.isArray(payload.myProcesses)).toBe(true); + expect(payload.messageId).toBe('m5'); + }); + + it('HASH_VALUE -> VALUE_HASHED avec hash', async () => { + const prev = postSpy.mock.calls.length; + dispatch({ type: MessageType.HASH_VALUE, accessToken: 'a', commitedIn: 'c', label: 'L', fileBlob: { type: 'application/octet-stream', data: new Uint8Array([1]) }, messageId: 'm6' }); + const [payload, targetOrigin] = (await waitForLastCall(MessageType.VALUE_HASHED, prev)) as any; + expect(targetOrigin).toBe(ORIGIN); + expect(payload.type).toBe(MessageType.VALUE_HASHED); + expect(payload.hash).toBe('hash-abc'); + expect(payload.messageId).toBe('m6'); + }); +});