From 28dc34382d986d286434407e4c20bafddc699368 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 5 Mar 2024 16:21:24 +0100 Subject: [PATCH 01/21] refactor: adjust customProtocolSaga code to be similar to deepLinkSaga; Move parsing code from main.ts to saga --- packages/common/src/index.ts | 1 + packages/common/src/invitationCode.ts | 9 +- packages/desktop/src/main/invitation.ts | 10 +- packages/desktop/src/main/main.ts | 18 +- packages/desktop/src/renderer/index.tsx | 16 +- .../invitation/customProtocol.saga.test.ts | 174 +++++++++--------- .../sagas/invitation/customProtocol.saga.ts | 115 ++++++++++-- .../src/rtl-tests/customProtocol.test.tsx | 2 +- .../src/rtl-tests/deep.linking.test.tsx | 4 +- .../src/store/init/deepLink/deepLink.saga.ts | 2 +- .../areObjectsEqual/areObjectsEqual.ts | 3 - .../sagas/communities/communities.slice.ts | 2 +- 12 files changed, 222 insertions(+), 134 deletions(-) delete mode 100644 packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f15cd34520..f7bf832b7f 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -11,3 +11,4 @@ export * from './libp2p' export * from './tests' export * from './auth' export * from './messages' +export * from './compare' diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 470fb6536e..af27c12d7b 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -23,7 +23,7 @@ const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDe if (!expectedProtocol) { // Create a full url to be able to use the same URL parsing mechanism expectedProtocol = `${DEEP_URL_SCHEME}:` - _url = `${DEEP_URL_SCHEME}://?${url}` + _url = `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${url}` } try { @@ -148,11 +148,12 @@ const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => export const argvInvitationCode = (argv: string[]): InvitationData | null => { let invitationData: InvitationData | null = null for (const arg of argv) { - try { - invitationData = parseInvitationCodeDeepUrl(arg) - } catch (e) { + if (!arg.startsWith(DEEP_URL_SCHEME_WITH_SEPARATOR)) { + console.log('Not a deep url, not parsing', arg) continue } + console.log('Parsing deep url', arg) + invitationData = parseInvitationCodeDeepUrl(arg) if (invitationData.pairs.length > 0) { break } else { diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index 2616059683..d73f0d2928 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -3,15 +3,11 @@ import path from 'path' import os from 'os' import { execSync } from 'child_process' import { BrowserWindow } from 'electron' -import { InvitationData, InvitationPair } from '@quiet/types' -export const processInvitationCode = (mainWindow: BrowserWindow, data: InvitationData | null) => { - if (!data || data?.pairs.length === 0) { - console.log('No valid invitation codes, not processing') - return - } +export const processInvitationCode = (mainWindow: BrowserWindow, code: string | string[]) => { + console.log('processInvitationCode:', code) mainWindow.webContents.send('invitation', { - data, + code, }) } diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index b1b65199b4..3b5022673d 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -79,8 +79,9 @@ if (!gotTheLock) { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() - const invitationCode = argvInvitationCode(commandLine) - processInvitationCode(mainWindow, invitationCode) + // const invitationCode = argvInvitationCode(commandLine) + // TODO: what should we do if there is no invitation code? Do nothing? + processInvitationCode(mainWindow, commandLine) } }) } @@ -157,8 +158,8 @@ app.on('open-url', (event, url) => { if (mainWindow) { invitationUrl = null try { - const invitationData = parseInvitationCodeDeepUrl(url) - processInvitationCode(mainWindow, invitationData) + // const invitationData = parseInvitationCodeDeepUrl(url) + processInvitationCode(mainWindow, url) } catch (e) { console.warn(e.message) } @@ -494,8 +495,8 @@ app.on('ready', async () => { } if (process.platform === 'darwin' && invitationUrl) { try { - const invitationData = parseInvitationCodeDeepUrl(invitationUrl) - processInvitationCode(mainWindow, invitationData) + // const invitationData = parseInvitationCodeDeepUrl(invitationUrl) + processInvitationCode(mainWindow, invitationUrl) } catch (e) { console.warn(e.message) } finally { @@ -503,9 +504,10 @@ app.on('ready', async () => { } } if (process.platform !== 'darwin' && process.argv) { + // TODO: when argv is used? try { - const invitationCode = argvInvitationCode(process.argv) - processInvitationCode(mainWindow, invitationCode) + // const invitationCode = argvInvitationCode(process.argv) + processInvitationCode(mainWindow, process.argv) } catch (e) { console.warn(e.message) } diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 3c478402b2..c73254eb47 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -20,10 +20,18 @@ ipcRenderer.on('force-save-state', async _event => { ipcRenderer.send('state-saved') }) -ipcRenderer.on('invitation', (_event, invitation: { data: InvitationData }) => { - if (!invitation.data) return - console.log('invitation', invitation.data.pairs, 'dispatching action') - store.dispatch(communities.actions.customProtocol(invitation.data)) +ipcRenderer.on('invitation', (_event, invitation: { code: string | string[] }) => { + console.log('ipcRenderer.on(invitation)', invitation) + if (!invitation.code) return + + let invitationData: string[] + if (typeof invitation.code === 'string') { + invitationData = [invitation.code] + } else { + invitationData = invitation.code + } + console.log('invitation', invitationData, 'dispatching action') + store.dispatch(communities.actions.customProtocol(invitationData)) }) ipcRenderer.on('socketIOSecret', (_event, socketIOSecret) => { diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts index ac732ee519..94ce563b68 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts @@ -31,97 +31,97 @@ describe('Handle invitation code', () => { validInvitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).data }) - it('creates network if code is valid', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: validInvitationData.pairs, - psk: validInvitationData.psk, - ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, - } - await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) - .withState(store.getState()) - .put(communities.actions.createNetwork(payload)) - .run() - }) + // it('creates network if code is valid', async () => { + // const payload: CreateNetworkPayload = { + // ownership: CommunityOwnership.User, + // peers: validInvitationData.pairs, + // psk: validInvitationData.psk, + // ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, + // } + // await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) + // .withState(store.getState()) + // .put(communities.actions.createNetwork(payload)) + // .run() + // }) - it('does not try to create network if user is already in community', async () => { - community = await factory.create['payload']>('Community') - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: validInvitationData.pairs, - psk: validInvitationData.psk, - } + // it('does not try to create network if user is already in community', async () => { + // community = await factory.create['payload']>('Community') + // const payload: CreateNetworkPayload = { + // ownership: CommunityOwnership.User, + // peers: validInvitationData.pairs, + // psk: validInvitationData.psk, + // } - await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) - .withState(store.getState()) - .put( - modalsActions.openModal({ - name: ModalName.warningModal, - args: { - title: 'You already belong to a community', - subtitle: "We're sorry but for now you can only be a member of a single community at a time.", - }, - }) - ) - .not.put(communities.actions.createNetwork(payload)) - .run() - }) + // await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) + // .withState(store.getState()) + // .put( + // modalsActions.openModal({ + // name: ModalName.warningModal, + // args: { + // title: 'You already belong to a community', + // subtitle: "We're sorry but for now you can only be a member of a single community at a time.", + // }, + // }) + // ) + // .not.put(communities.actions.createNetwork(payload)) + // .run() + // }) - it('does not try to create network if code is missing addresses', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: [], - } + // it('does not try to create network if code is missing addresses', async () => { + // const payload: CreateNetworkPayload = { + // ownership: CommunityOwnership.User, + // peers: [], + // } - await expectSaga( - customProtocolSaga, - communities.actions.customProtocol({ - pairs: [], - psk: '12345', - ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - }) - ) - .withState(store.getState()) - .put(communities.actions.clearInvitationCodes()) - .put( - modalsActions.openModal({ - name: ModalName.warningModal, - args: { - title: 'Invalid link', - subtitle: 'The invite link you received is not valid. Please check it and try again.', - }, - }) - ) - .not.put(communities.actions.createNetwork(payload)) - .run() - }) + // await expectSaga( + // customProtocolSaga, + // communities.actions.customProtocol({ + // pairs: [], + // psk: '12345', + // ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', + // }) + // ) + // .withState(store.getState()) + // .put(communities.actions.clearInvitationCodes()) + // .put( + // modalsActions.openModal({ + // name: ModalName.warningModal, + // args: { + // title: 'Invalid link', + // subtitle: 'The invite link you received is not valid. Please check it and try again.', + // }, + // }) + // ) + // .not.put(communities.actions.createNetwork(payload)) + // .run() + // }) - it('does not try to create network if code is missing psk', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: [], - } + // it('does not try to create network if code is missing psk', async () => { + // const payload: CreateNetworkPayload = { + // ownership: CommunityOwnership.User, + // peers: [], + // } - await expectSaga( - customProtocolSaga, - communities.actions.customProtocol({ - pairs: validInvitationData.pairs, - psk: '', - ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - }) - ) - .withState(store.getState()) - .put(communities.actions.clearInvitationCodes()) - .put( - modalsActions.openModal({ - name: ModalName.warningModal, - args: { - title: 'Invalid link', - subtitle: 'The invite link you received is not valid. Please check it and try again.', - }, - }) - ) - .not.put(communities.actions.createNetwork(payload)) - .run() - }) + // await expectSaga( + // customProtocolSaga, + // communities.actions.customProtocol({ + // pairs: validInvitationData.pairs, + // psk: '', + // ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', + // }) + // ) + // .withState(store.getState()) + // .put(communities.actions.clearInvitationCodes()) + // .put( + // modalsActions.openModal({ + // name: ModalName.warningModal, + // args: { + // title: 'Invalid link', + // subtitle: 'The invite link you received is not valid. Please check it and try again.', + // }, + // }) + // ) + // .not.put(communities.actions.createNetwork(payload)) + // .run() + // }) }) diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 98a26e3035..37f723316b 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -1,14 +1,20 @@ import { PayloadAction } from '@reduxjs/toolkit' import { select, put, delay } from 'typed-redux-saga' -import { CommunityOwnership, CreateNetworkPayload } from '@quiet/types' -import { communities } from '@quiet/state-manager' +import { CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' +import { communities, getInvitationCodes } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' import { ModalName } from '../modals/modals.types' import { modalsActions } from '../modals/modals.slice' +import { areObjectsEqual, argvInvitationCode } from '@quiet/common' export function* customProtocolSaga( action: PayloadAction['payload']> ): Generator { + // TODO: refactor to remove code duplication. This is a slightly adjusted code from deepLink.saga.ts + const code = action.payload + + console.log('INIT_NAVIGATION: Waiting for websocket connection before proceeding with deep link flow.') + while (true) { const connected = yield* select(socketSelectors.isConnected) if (connected) { @@ -17,8 +23,80 @@ export function* customProtocolSaga( yield* delay(500) } + console.log('INIT_NAVIGATION: Continuing on deep link flow.') + + let data: InvitationData | null + + try { + data = argvInvitationCode(code) + } catch (e) { + console.warn(e.message) + + yield* put(communities.actions.clearInvitationCodes()) + yield* put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'Invalid link', + subtitle: 'The invite link you received is not valid. Please check it and try again.', + }, + }) + ) + return + } + + if (data === null) { + console.log(`Not processing invitation code ${code}`) + return + } + const community = yield* select(communities.selectors.currentCommunity) - if (community) { + + const storedInvitationCodes = yield* select(communities.selectors.invitationCodes) + const currentInvitationCodes = data.pairs + + console.log('Stored invitation codes', storedInvitationCodes) + console.log('Current invitation codes', currentInvitationCodes) + + let isInvitationDataValid = false + + if (storedInvitationCodes.length === 0) { + isInvitationDataValid = true + } else { + isInvitationDataValid = storedInvitationCodes.some(storedCode => + currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) + ) + } + + console.log('Is invitation data valid', isInvitationDataValid) + + const isAlreadyConnected = Boolean(community?.name) + + const alreadyBelongsWithAnotherCommunity = !isInvitationDataValid && isAlreadyConnected + const connectingWithAnotherCommunity = !isInvitationDataValid && !isAlreadyConnected + const alreadyBelongsWithCurrentCommunity = isInvitationDataValid && isAlreadyConnected + const connectingWithCurrentCommunity = isInvitationDataValid && !isAlreadyConnected + + if (alreadyBelongsWithAnotherCommunity) { + console.log('INIT_NAVIGATION: ABORTING: Already belongs with another community.') + } + + if (connectingWithAnotherCommunity) { + console.log('INIT_NAVIGATION: ABORTING: Proceeding with connection to another community.') + } + + if (alreadyBelongsWithCurrentCommunity) { + console.log('INIT_NAVIGATION: ABORTING: Already connected with the current community.') + } + + if (connectingWithCurrentCommunity) { + console.log('INIT_NAVIGATION: Proceeding with connection to the community.') + } + + // User already belongs to a community + if (alreadyBelongsWithAnotherCommunity || alreadyBelongsWithCurrentCommunity) { + console.log('INIT_NAVIGATION: Displaying error (user already belongs to a community).') + yield* put(communities.actions.clearInvitationCodes()) // TODO: check out yield* put( modalsActions.openModal({ name: ModalName.warningModal, @@ -28,27 +106,32 @@ export function* customProtocolSaga( }, }) ) + return } - const invitationData = action.payload - if (invitationData && invitationData.pairs.length > 0 && invitationData.psk && invitationData.ownerOrbitDbIdentity) { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: invitationData.pairs, - psk: invitationData.psk, - ownerOrbitDbIdentity: invitationData.ownerOrbitDbIdentity, - } - yield* put(communities.actions.createNetwork(payload)) - } else { - yield* put(communities.actions.clearInvitationCodes()) + + if (connectingWithAnotherCommunity) { + console.log('INIT_NAVIGATION: Displaying error (user is already connecting to another community).') + yield* put(communities.actions.clearInvitationCodes()) // TODO: check out yield* put( modalsActions.openModal({ name: ModalName.warningModal, args: { - title: 'Invalid link', - subtitle: 'The invite link you received is not valid. Please check it and try again.', + title: 'You already started to connect to another community', + subtitle: "We're sorry but for now you can only be a member of a single community at a time.", }, }) ) + + return + } + + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, } + console.log('INIT_NAVIGATION: Creating network with payload', payload) + yield* put(communities.actions.createNetwork(payload)) } diff --git a/packages/desktop/src/rtl-tests/customProtocol.test.tsx b/packages/desktop/src/rtl-tests/customProtocol.test.tsx index 9cacc8ece9..1259557f98 100644 --- a/packages/desktop/src/rtl-tests/customProtocol.test.tsx +++ b/packages/desktop/src/rtl-tests/customProtocol.test.tsx @@ -65,7 +65,7 @@ describe('Opening app through custom protocol', () => { ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', } - store.dispatch(communities.actions.customProtocol(invitationCodes)) + // store.dispatch(communities.actions.customProtocol(invitationCodes)) store.dispatch(modalsActions.openModal({ name: ModalName.joinCommunityModal })) diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index 4130b7bc14..4c7b881d14 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -32,13 +32,13 @@ describe('Deep linking', () => { renderComponent(<>, store) - store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[0])) + // store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[0])) await act(async () => {}) const originalPair = communities.selectors.invitationCodes(store.getState()) // Redo the action to provoke renewed saga runs - store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[1])) + // store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[1])) await act(async () => {}) const currentPair = communities.selectors.invitationCodes(store.getState()) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index 4c98593786..e246d98cba 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -8,7 +8,7 @@ import { initActions } from '../init.slice' import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' import { CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' -import { areObjectsEqual } from '../../../utils/functions/areObjectsEqual/areObjectsEqual' +import { areObjectsEqual } from '@quiet/common' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { const code = action.payload diff --git a/packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts b/packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts deleted file mode 100644 index d2f9899e4f..0000000000 --- a/packages/mobile/src/utils/functions/areObjectsEqual/areObjectsEqual.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const areObjectsEqual = (obj1: any, obj2: any): boolean => { - return JSON.stringify(obj1) === JSON.stringify(obj2) -} diff --git a/packages/state-manager/src/sagas/communities/communities.slice.ts b/packages/state-manager/src/sagas/communities/communities.slice.ts index f1d0e471d6..31e59220a2 100644 --- a/packages/state-manager/src/sagas/communities/communities.slice.ts +++ b/packages/state-manager/src/sagas/communities/communities.slice.ts @@ -52,7 +52,7 @@ export const communitiesSlice = createSlice({ }, resetApp: (state, _action) => state, launchCommunity: (state, _action: PayloadAction) => state, - customProtocol: (state, _action: PayloadAction) => state, + customProtocol: (state, _action: PayloadAction) => state, setInvitationCodes: (state, action: PayloadAction) => { state.invitationCodes = action.payload }, From b8aa768d50f12adb278333228f1ac0104f1625e8 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 5 Mar 2024 17:30:45 +0100 Subject: [PATCH 02/21] fix: customProtocolSaga tests --- .../invitation/customProtocol.saga.test.ts | 170 +++++++++--------- 1 file changed, 83 insertions(+), 87 deletions(-) diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts index 94ce563b68..0a8402d75f 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts @@ -15,6 +15,7 @@ describe('Handle invitation code', () => { let factory: FactoryGirl let community: Community let validInvitationData: InvitationData + let validInvitationDeepUrl: string beforeEach(async () => { store = ( @@ -29,99 +30,94 @@ describe('Handle invitation code', () => { factory = await getFactory(store) validInvitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).data + validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).deepUrl() }) - // it('creates network if code is valid', async () => { - // const payload: CreateNetworkPayload = { - // ownership: CommunityOwnership.User, - // peers: validInvitationData.pairs, - // psk: validInvitationData.psk, - // ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, - // } - // await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) - // .withState(store.getState()) - // .put(communities.actions.createNetwork(payload)) - // .run() - // }) + it('creates network if code is valid', async () => { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: validInvitationData.pairs, + psk: validInvitationData.psk, + ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, + } + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) + .withState(store.getState()) + .put(communities.actions.createNetwork(payload)) + .run() + }) - // it('does not try to create network if user is already in community', async () => { - // community = await factory.create['payload']>('Community') - // const payload: CreateNetworkPayload = { - // ownership: CommunityOwnership.User, - // peers: validInvitationData.pairs, - // psk: validInvitationData.psk, - // } + it('does not try to create network if user is already in community', async () => { + community = await factory.create['payload']>('Community') + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: validInvitationData.pairs, + psk: validInvitationData.psk, + } - // await expectSaga(customProtocolSaga, communities.actions.customProtocol(validInvitationData)) - // .withState(store.getState()) - // .put( - // modalsActions.openModal({ - // name: ModalName.warningModal, - // args: { - // title: 'You already belong to a community', - // subtitle: "We're sorry but for now you can only be a member of a single community at a time.", - // }, - // }) - // ) - // .not.put(communities.actions.createNetwork(payload)) - // .run() - // }) + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) + .withState(store.getState()) + .put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'You already belong to a community', + subtitle: "We're sorry but for now you can only be a member of a single community at a time.", + }, + }) + ) + .not.put(communities.actions.createNetwork(payload)) + .run() + }) - // it('does not try to create network if code is missing addresses', async () => { - // const payload: CreateNetworkPayload = { - // ownership: CommunityOwnership.User, - // peers: [], - // } + it('does not try to create network if code is missing psk', async () => { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: [], + } - // await expectSaga( - // customProtocolSaga, - // communities.actions.customProtocol({ - // pairs: [], - // psk: '12345', - // ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - // }) - // ) - // .withState(store.getState()) - // .put(communities.actions.clearInvitationCodes()) - // .put( - // modalsActions.openModal({ - // name: ModalName.warningModal, - // args: { - // title: 'Invalid link', - // subtitle: 'The invite link you received is not valid. Please check it and try again.', - // }, - // }) - // ) - // .not.put(communities.actions.createNetwork(payload)) - // .run() - // }) + await expectSaga( + customProtocolSaga, + communities.actions.customProtocol(['someArg', 'quiet://?k=BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=']) + ) + .withState(store.getState()) + .put(communities.actions.clearInvitationCodes()) + .put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'Invalid link', + subtitle: 'The invite link you received is not valid. Please check it and try again.', + }, + }) + ) + .not.put(communities.actions.createNetwork(payload)) + .run() + }) - // it('does not try to create network if code is missing psk', async () => { - // const payload: CreateNetworkPayload = { - // ownership: CommunityOwnership.User, - // peers: [], - // } + it('does not try to create network if code is missing psk', async () => { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: [], + } - // await expectSaga( - // customProtocolSaga, - // communities.actions.customProtocol({ - // pairs: validInvitationData.pairs, - // psk: '', - // ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', - // }) - // ) - // .withState(store.getState()) - // .put(communities.actions.clearInvitationCodes()) - // .put( - // modalsActions.openModal({ - // name: ModalName.warningModal, - // args: { - // title: 'Invalid link', - // subtitle: 'The invite link you received is not valid. Please check it and try again.', - // }, - // }) - // ) - // .not.put(communities.actions.createNetwork(payload)) - // .run() - // }) + await expectSaga( + customProtocolSaga, + communities.actions.customProtocol([ + 'quiet://?QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + ]) + ) + .withState(store.getState()) + .put(communities.actions.clearInvitationCodes()) + .put( + modalsActions.openModal({ + name: ModalName.warningModal, + args: { + title: 'Invalid link', + subtitle: 'The invite link you received is not valid. Please check it and try again.', + }, + }) + ) + .not.put(communities.actions.createNetwork(payload)) + .run() + }) }) From 1460d575cce8fbcda66605bf8a753c0089943647 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 5 Mar 2024 17:51:00 +0100 Subject: [PATCH 03/21] fix: tests; add missing file --- packages/common/src/compare.ts | 4 ++++ packages/desktop/src/rtl-tests/customProtocol.test.tsx | 3 ++- packages/desktop/src/rtl-tests/deep.linking.test.tsx | 10 +++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/common/src/compare.ts diff --git a/packages/common/src/compare.ts b/packages/common/src/compare.ts new file mode 100644 index 0000000000..10db5fdbc0 --- /dev/null +++ b/packages/common/src/compare.ts @@ -0,0 +1,4 @@ +export const areObjectsEqual = (obj1: any, obj2: any): boolean => { + // Using this only makes sense for small objects whose properties are in the same order + return JSON.stringify(obj1) === JSON.stringify(obj2) +} diff --git a/packages/desktop/src/rtl-tests/customProtocol.test.tsx b/packages/desktop/src/rtl-tests/customProtocol.test.tsx index 1259557f98..1a299c2d6d 100644 --- a/packages/desktop/src/rtl-tests/customProtocol.test.tsx +++ b/packages/desktop/src/rtl-tests/customProtocol.test.tsx @@ -11,6 +11,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import JoinCommunity from '../renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity' import CreateUsername from '../renderer/components/CreateUsername/CreateUsername' import { type Community, type InvitationData } from '@quiet/types' +import { composeInvitationDeepUrl } from '@quiet/common' jest.setTimeout(20_000) @@ -65,7 +66,7 @@ describe('Opening app through custom protocol', () => { ownerOrbitDbIdentity: 'testOwnerOrbitDbIdentity', } - // store.dispatch(communities.actions.customProtocol(invitationCodes)) + store.dispatch(communities.actions.customProtocol([composeInvitationDeepUrl(invitationCodes)])) store.dispatch(modalsActions.openModal({ name: ModalName.joinCommunityModal })) diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index f4d80943fc..464b9ad6e6 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -7,7 +7,7 @@ import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' import { prepareStore } from '../renderer/testUtils/prepareStore' import { renderComponent } from '../renderer/testUtils/renderComponent' -import { validInvitationCodeTestData } from '@quiet/common' +import { getValidInvitationUrlTestData, validInvitationCodeTestData } from '@quiet/common' import { communities } from '@quiet/state-manager' describe('Deep linking', () => { @@ -34,13 +34,17 @@ describe('Deep linking', () => { renderComponent(<>, store) - // store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[0])) + store.dispatch( + communities.actions.customProtocol([getValidInvitationUrlTestData(validInvitationCodeTestData[0]).deepUrl()]) + ) await act(async () => {}) const originalPair = communities.selectors.invitationCodes(store.getState()) // Redo the action to provoke renewed saga runs - // store.dispatch(communities.actions.customProtocol(validInvitationCodeTestData[1])) + store.dispatch( + communities.actions.customProtocol([getValidInvitationUrlTestData(validInvitationCodeTestData[1]).deepUrl()]) + ) await act(async () => {}) const currentPair = communities.selectors.invitationCodes(store.getState()) From b4e8f457297ff16e4fc6441a58282689fbff0c7e Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 6 Mar 2024 14:26:12 +0100 Subject: [PATCH 04/21] fix: main.ts test --- packages/desktop/src/main/invitation.ts | 1 + packages/desktop/src/main/main.test.ts | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index d73f0d2928..cfc1bf429a 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -6,6 +6,7 @@ import { BrowserWindow } from 'electron' export const processInvitationCode = (mainWindow: BrowserWindow, code: string | string[]) => { console.log('processInvitationCode:', code) + if (!code || !code.length) return mainWindow.webContents.send('invitation', { code, }) diff --git a/packages/desktop/src/main/main.test.ts b/packages/desktop/src/main/main.test.ts index dca57a360c..66d2873457 100644 --- a/packages/desktop/src/main/main.test.ts +++ b/packages/desktop/src/main/main.test.ts @@ -250,27 +250,28 @@ describe('Invitation code', () => { expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) + const deepUrl = composeInvitationDeepUrl(codes) + mockAppOnCalls[1][1](event, deepUrl) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: deepUrl }) }) - it('do not process invitation code on open-url event (on macos) if url is invalid', async () => { - codes['psk'] = '12345' + it('do not process invitation code on open-url event (on macos) if url is empty', async () => { expect(mockAppOnCalls[2][0]).toBe('ready') await mockAppOnCalls[2][1]() expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) - expect(mockWindowWebContentsSend).not.toHaveBeenCalledWith('invitation', { data: codes }) + mockAppOnCalls[1][1](event, '') + expect(mockWindowWebContentsSend).not.toHaveBeenCalledWith('invitation', { code: '' }) }) it('process invitation code on second-instance event', async () => { await mockAppOnCalls[2][1]() - const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', composeInvitationDeepUrl(codes)] + const deepUrl = composeInvitationDeepUrl(codes) + const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', deepUrl, 'something/else'] expect(mockAppOnCalls[0][0]).toBe('second-instance') const event = { preventDefault: () => {} } mockAppOnCalls[0][1](event, commandLine) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { code: deepUrl }) }) }) From 7d0b19aa5eea423effe69754fecf2c079a6df834 Mon Sep 17 00:00:00 2001 From: Emi Date: Thu, 7 Mar 2024 16:00:07 +0100 Subject: [PATCH 05/21] feat: handle old (psk, orbitdbIdentity, addresses) and new (cid, token, serverAddress, inviterAddress) invitation link format --- packages/common/src/invitationCode.test.ts | 16 +- packages/common/src/invitationCode.ts | 145 ++++++++++++++---- packages/desktop/src/main/main.ts | 12 +- .../sagas/invitation/customProtocol.saga.ts | 33 ++-- .../src/store/init/deepLink/deepLink.saga.ts | 27 +++- .../connection.selectors.test.ts | 5 +- .../appConnection/connection.selectors.ts | 5 +- .../communities/communities.selectors.test.ts | 2 - .../invitationCode/invitationCode.ts | 2 +- packages/types/src/network.ts | 25 ++- 10 files changed, 195 insertions(+), 77 deletions(-) diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 5f18ae4378..91eaa5d55d 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,11 +1,11 @@ -import { InvitationData } from '@quiet/types' +import { InvitationData, InvitationPair } from '@quiet/types' import { argvInvitationCode, composeInvitationDeepUrl, - invitationShareUrl, composeInvitationShareUrl, parseInvitationCodeDeepUrl, PSK_PARAM_KEY, + p2pAddressesToPairs, } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' @@ -75,15 +75,17 @@ describe('Invitation code helper', () => { expect(composeInvitationShareUrl(pairs)).toEqual(expected) }) - it('builds proper invitation share url from peers addresses', () => { + it('converts list of p2p addresses to invitation pairs', () => { + const pair: InvitationPair = { + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + onionAddress: 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad', + } const peerList = [ - '/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + `/dns4/${pair.onionAddress}.onion/tcp/443/wss/p2p/${pair.peerId}`, 'invalidAddress', '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', ] - expect(invitationShareUrl(peerList, pskDecoded, ownerOrbitDbIdentity)).toEqual( - `${QUIET_JOIN_PAGE}#QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad&QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA=somethingElse&${PSK_PARAM_KEY}=${psk}` - ) + expect(p2pAddressesToPairs(peerList)).toEqual([pair]) }) it('retrieves invitation codes from deep url', () => { diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index af27c12d7b..38a90c0bb7 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,11 +1,19 @@ -import { InvitationData, InvitationPair } from '@quiet/types' +import { InvitationData, InvitationDataV1, InvitationDataV2, InvitationDataVersion, InvitationPair } from '@quiet/types' import { QUIET_JOIN_PAGE } from './static' import { createLibp2pAddress, isPSKcodeValid } from './libp2p' import Logger from './logger' const logger = Logger('invite') +// V1 invitation code format (current) export const PSK_PARAM_KEY = 'k' export const OWNER_ORBIT_DB_IDENTITY_PARAM_KEY = 'o' + +// V2 invitation code format (new) +export const CID_PARAM_KEY = 'c' +export const TOKEN_PARAM_KEY = 't' +export const INVITER_ADDRESS_PARAM_KEY = 'i' +export const SERVER_ADDRESS_PARAM_KEY = 's' + const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' const DEEP_URL_SCHEME = 'quiet' const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g @@ -16,31 +24,52 @@ interface ParseDeepUrlParams { expectedProtocol?: string } -const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDeepUrlParams): InvitationData => { - let _url = url - let validUrl: URL | null = null +const parseCodeV2 = (url: string): InvitationDataV2 => { + const params = new URL(url).searchParams - if (!expectedProtocol) { - // Create a full url to be able to use the same URL parsing mechanism - expectedProtocol = `${DEEP_URL_SCHEME}:` - _url = `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${url}` - } + const cid = params.get(CID_PARAM_KEY) + if (!cid) throw new Error(`No cid found in invitation code '${url}'`) + // TODO: Validate CID format + params.delete(CID_PARAM_KEY) + let token = params.get(TOKEN_PARAM_KEY) + if (!token) throw new Error(`No token found in invitation code '${url}'`) + token = decodeURIComponent(token) + // TODO: validate token format + params.delete(TOKEN_PARAM_KEY) + + let serverAddress = params.get(SERVER_ADDRESS_PARAM_KEY) + if (!serverAddress) throw new Error(`No server address found in invitation code '${url}'`) + serverAddress = decodeURIComponent(serverAddress) try { - validUrl = new URL(_url) + new URL(url) } catch (e) { - logger.error(`Could not retrieve invitation code from deep url '${url}'. Reason: ${e.message}`) - throw e + throw new Error(`Invalid server address format '${url}'`) } - if (!validUrl || validUrl.protocol !== expectedProtocol) { - logger.error(`Could not retrieve invitation code from deep url '${url}'`) - throw new Error(`Invalid url`) + params.delete(SERVER_ADDRESS_PARAM_KEY) + + let inviterAddress = params.get(INVITER_ADDRESS_PARAM_KEY) // TODO: can it be also peerId-onionAddress pair? + if (!inviterAddress) throw new Error(`No inviter address in invitation code '${url}'`) + inviterAddress = decodeURIComponent(inviterAddress) + if (!inviterAddress.trim().match(ONION_ADDRESS_REGEX)) { + throw new Error(`No inviter address in invitation code '${url}'`) } + params.delete(INVITER_ADDRESS_PARAM_KEY) - const params = validUrl.searchParams - const codes: InvitationPair[] = [] + return { + version: InvitationDataVersion.v2, + cid, + token, + serverAddress, + inviterAddress, + } +} + +const parseCodeV1 = (url: string): InvitationDataV1 => { + const params = new URL(url).searchParams let psk = params.get(PSK_PARAM_KEY) + const codes: InvitationPair[] = [] if (!psk) throw new Error(`No psk found in invitation code '${url}'`) psk = decodeURIComponent(psk) if (!isPSKcodeValid(psk)) throw new Error(`Invalid psk in invitation code '${url}'`) @@ -58,14 +87,54 @@ const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDe onionAddress, }) }) - logger('Retrieved data:', codes) return { + version: InvitationDataVersion.v1, pairs: codes, psk, ownerOrbitDbIdentity, } } +const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDeepUrlParams): InvitationData => { + let _url = url + let validUrl: URL | null = null + + if (!expectedProtocol) { + // Create a full url to be able to use the same URL parsing mechanism + expectedProtocol = `${DEEP_URL_SCHEME}:` + _url = `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${url}` + } + + try { + validUrl = new URL(_url) + } catch (e) { + logger.error(`Could not retrieve invitation code from deep url '${url}'. Reason: ${e.message}`) + throw e + } + if (!validUrl || validUrl.protocol !== expectedProtocol) { + logger.error(`Could not retrieve invitation code from deep url '${url}'`) + throw new Error(`Invalid url`) + } + + const params = validUrl.searchParams + + const psk = params.get(PSK_PARAM_KEY) + const cid = params.get(CID_PARAM_KEY) + if (!psk && !cid) throw new Error(`Invitation code does not match either v1 or v2 format '${url}'`) + + let data: InvitationData + if (psk) { + data = parseCodeV1(_url) + } else { + data = parseCodeV2(_url) + } + + if (!data) throw new Error(`Could not parse invitation code from deep url '${url}'`) + + logger(`Invitation data '${data}' parsed`) + return data +} + /** * Extract invitation data from deep url. * Valid format: quiet://?=&=&k= @@ -81,15 +150,12 @@ export const parseInvitationCode = (code: string): InvitationData => { return parseDeepUrl({ url: code, expectedProtocol: '' }) } -/** - * @arg {string[]} peers - List of peer's p2p addresses - * @arg psk - Pre shared key in base64 - * @returns {string} - Complete shareable invitation link, e.g. - * https://tryquiet.org/join/#=&=&k=&o= - */ -export const invitationShareUrl = (peers: string[] = [], psk: string, ownerOrbitDbIdentity: string): string => { +export const p2pAddressesToPairs = (addresses: string[]): InvitationPair[] => { + /** + * @arg {string[]} addresses - List of peer's p2p addresses + */ const pairs: InvitationPair[] = [] - for (const peerAddress of peers) { + for (const peerAddress of addresses) { let peerId: string let onionAddress: string try { @@ -112,8 +178,7 @@ export const invitationShareUrl = (peers: string[] = [], psk: string, ownerOrbit const rawAddress = onionAddress.endsWith('.onion') ? onionAddress.split('.')[0] : onionAddress pairs.push({ peerId: peerId, onionAddress: rawAddress }) } - - return composeInvitationShareUrl({ pairs, psk, ownerOrbitDbIdentity }) + return pairs } export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { @@ -125,6 +190,10 @@ export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { } export const composeInvitationShareUrl = (data: InvitationData) => { + /** + * @returns {string} - Complete shareable invitation link, e.g. + * https://tryquiet.org/join/#=&=&k=&o= + */ return composeInvitationUrl(`${QUIET_JOIN_PAGE}`, data).replace('?', '#') } @@ -134,11 +203,21 @@ export const composeInvitationDeepUrl = (data: InvitationData): string => { const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => { const url = new URL(baseUrl) - for (const pair of data.pairs) { - url.searchParams.append(pair.peerId, pair.onionAddress) + + if (!data.version || data.version === InvitationDataVersion.v1) { + if (!data.pairs || !data.psk || !data.ownerOrbitDbIdentity) return '' // TODO: temporary until better solution is found + for (const pair of data.pairs) { + url.searchParams.append(pair.peerId, pair.onionAddress) + } + url.searchParams.append(PSK_PARAM_KEY, data.psk) + url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) + } else if (data.version === InvitationDataVersion.v2) { + if (!data.cid || !data.token || !data.serverAddress || !data.inviterAddress) return '' // TODO: temporary until better solution is found + url.searchParams.append(CID_PARAM_KEY, data.cid) + url.searchParams.append(TOKEN_PARAM_KEY, data.token) + url.searchParams.append(SERVER_ADDRESS_PARAM_KEY, data.serverAddress) + url.searchParams.append(INVITER_ADDRESS_PARAM_KEY, data.inviterAddress) } - url.searchParams.append(PSK_PARAM_KEY, data.psk) - url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) return url.href } @@ -154,7 +233,7 @@ export const argvInvitationCode = (argv: string[]): InvitationData | null => { } console.log('Parsing deep url', arg) invitationData = parseInvitationCodeDeepUrl(arg) - if (invitationData.pairs.length > 0) { + if (invitationData.pairs && invitationData.pairs.length > 0) { break } else { invitationData = null diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index 3b5022673d..7b0c4b96be 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -79,8 +79,6 @@ if (!gotTheLock) { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() - // const invitationCode = argvInvitationCode(commandLine) - // TODO: what should we do if there is no invitation code? Do nothing? processInvitationCode(mainWindow, commandLine) } }) @@ -157,12 +155,7 @@ app.on('open-url', (event, url) => { event.preventDefault() if (mainWindow) { invitationUrl = null - try { - // const invitationData = parseInvitationCodeDeepUrl(url) - processInvitationCode(mainWindow, url) - } catch (e) { - console.warn(e.message) - } + processInvitationCode(mainWindow, url) } }) @@ -495,7 +488,6 @@ app.on('ready', async () => { } if (process.platform === 'darwin' && invitationUrl) { try { - // const invitationData = parseInvitationCodeDeepUrl(invitationUrl) processInvitationCode(mainWindow, invitationUrl) } catch (e) { console.warn(e.message) @@ -504,9 +496,7 @@ app.on('ready', async () => { } } if (process.platform !== 'darwin' && process.argv) { - // TODO: when argv is used? try { - // const invitationCode = argvInvitationCode(process.argv) processInvitationCode(mainWindow, process.argv) } catch (e) { console.warn(e.message) diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 37f723316b..69c2c8f5c8 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -1,6 +1,6 @@ import { PayloadAction } from '@reduxjs/toolkit' import { select, put, delay } from 'typed-redux-saga' -import { CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' +import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataVersion } from '@quiet/types' import { communities, getInvitationCodes } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' import { ModalName } from '../modals/modals.types' @@ -52,20 +52,29 @@ export function* customProtocolSaga( const community = yield* select(communities.selectors.currentCommunity) - const storedInvitationCodes = yield* select(communities.selectors.invitationCodes) - const currentInvitationCodes = data.pairs - - console.log('Stored invitation codes', storedInvitationCodes) - console.log('Current invitation codes', currentInvitationCodes) - + // TODO: rename let isInvitationDataValid = false - if (storedInvitationCodes.length === 0) { - isInvitationDataValid = true + if (!data.version || data.version === InvitationDataVersion.v1) { + const storedInvitationCodes = yield* select(communities.selectors.invitationCodes) + const currentInvitationCodes = data.pairs + + console.log('Stored invitation codes', storedInvitationCodes) + console.log('Current invitation codes', currentInvitationCodes) + + if (!currentInvitationCodes) { + isInvitationDataValid = false + } else if (storedInvitationCodes.length === 0) { + isInvitationDataValid = true + } else { + // TODO: check if psk is the same instead + isInvitationDataValid = storedInvitationCodes.some(storedCode => + currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) + ) + } } else { - isInvitationDataValid = storedInvitationCodes.some(storedCode => - currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) - ) + // TODO: ? + isInvitationDataValid = true } console.log('Is invitation data valid', isInvitationDataValid) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index e246d98cba..dce40fe8e0 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -7,7 +7,7 @@ import { initSelectors } from '../init.selectors' import { initActions } from '../init.slice' import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' -import { CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' +import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataVersion } from '@quiet/types' import { areObjectsEqual } from '@quiet/common' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { @@ -55,14 +55,29 @@ export function* deepLinkSaga(action: PayloadAction + currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) + ) + } } else { - isInvitationDataValid = storedInvitationCodes.some(storedCode => - currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) - ) + // TODO: ? + isInvitationDataValid = true } console.log('Is invitation data valid', isInvitationDataValid) diff --git a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts index 3f298c92ea..f5dd10e583 100644 --- a/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts +++ b/packages/state-manager/src/sagas/appConnection/connection.selectors.test.ts @@ -7,7 +7,7 @@ import { communitiesActions } from '../communities/communities.slice' import { connectionActions } from './connection.slice' import { type FactoryGirl } from 'factory-girl' import { type Community } from '@quiet/types' -import { createLibp2pAddress, invitationShareUrl } from '@quiet/common' +import { composeInvitationShareUrl, createLibp2pAddress, p2pAddressesToPairs } from '@quiet/common' describe('communitiesSelectors', () => { setupCrypto() @@ -116,7 +116,8 @@ describe('communitiesSelectors', () => { }) store.dispatch(communitiesActions.savePSK(psk)) const selectorInvitationUrl = connectionSelectors.invitationUrl(store.getState()) - const expectedUrl = invitationShareUrl(peerList, psk, ownerOrbitDbIdentity) + const pairs = p2pAddressesToPairs(peerList) + const expectedUrl = composeInvitationShareUrl({ pairs, psk, ownerOrbitDbIdentity }) expect(expectedUrl).not.toEqual('') expect(selectorInvitationUrl).toEqual(expectedUrl) }) diff --git a/packages/state-manager/src/sagas/appConnection/connection.selectors.ts b/packages/state-manager/src/sagas/appConnection/connection.selectors.ts index 1bce105c4c..72f8cfd9f1 100644 --- a/packages/state-manager/src/sagas/appConnection/connection.selectors.ts +++ b/packages/state-manager/src/sagas/appConnection/connection.selectors.ts @@ -6,7 +6,7 @@ import { peersStatsAdapter } from './connection.adapter' import { connectedPeers, isCurrentCommunityInitialized } from '../network/network.selectors' import { type NetworkStats } from './connection.types' import { type User } from '../users/users.types' -import { filterAndSortPeers, invitationShareUrl } from '@quiet/common' +import { composeInvitationShareUrl, filterAndSortPeers, p2pAddressesToPairs } from '@quiet/common' import { areMessagesLoaded, areChannelsLoaded } from '../publicChannels/publicChannels.selectors' import { identitySelectors } from '../identity/identity.selectors' import { communitiesSelectors } from '../communities/communities.selectors' @@ -54,7 +54,8 @@ export const invitationUrl = createSelector( if (!communityPsk) return '' if (!ownerOrbitDbIdentity) return '' const initialPeers = sortedPeerList.slice(0, 3) - return invitationShareUrl(initialPeers, communityPsk, ownerOrbitDbIdentity) + const pairs = p2pAddressesToPairs(initialPeers) + return composeInvitationShareUrl({ pairs, psk: communityPsk, ownerOrbitDbIdentity }) } ) diff --git a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts index 1cccbaa515..52ffb5f7b1 100644 --- a/packages/state-manager/src/sagas/communities/communities.selectors.test.ts +++ b/packages/state-manager/src/sagas/communities/communities.selectors.test.ts @@ -1,10 +1,8 @@ -import { createLibp2pAddress, invitationShareUrl } from '@quiet/common' import { setupCrypto } from '@quiet/identity' import { type Store } from '@reduxjs/toolkit' import { getFactory } from '../../utils/tests/factories' import { prepareStore } from '../../utils/tests/prepareStore' import { type identityActions } from '../identity/identity.slice' -import { usersActions } from '../users/users.slice' import { communitiesSelectors } from './communities.selectors' import { communitiesActions } from './communities.slice' import { type Community, type Identity } from '@quiet/types' diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts index 1fe956f338..65eb75c065 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts @@ -32,7 +32,7 @@ export const getInvitationCodes = (codeOrUrl: string): InvitationData => { data = parseInvitationCode(code) - if (!data || data?.pairs.length === 0) { + if (!data || data?.pairs?.length === 0) { throw new Error(`No invitation codes. Code/url passed: ${codeOrUrl}`) } diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index 894ca4f427..2e8ce5bf2c 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -8,8 +8,31 @@ export type InvitationPair = { onionAddress: string } -export type InvitationData = { +export enum InvitationDataVersion { + v1 = 'v1', + v2 = 'v2', +} + +export type InvitationDataV1 = { + version?: InvitationDataVersion pairs: InvitationPair[] psk: string ownerOrbitDbIdentity: string } + +export type InvitationDataV2 = { + version?: InvitationDataVersion + cid: string + token: string + serverAddress: string + inviterAddress: string +} + +// export type InvitationData = { +// version?: InvitationDataVersion +// pairs: InvitationPair[] +// psk: string +// ownerOrbitDbIdentity: string +// } + +export type InvitationData = Partial & Partial From d386067e68b1e669975e201519a44e313c54dfe7 Mon Sep 17 00:00:00 2001 From: Emi Date: Mon, 11 Mar 2024 12:02:23 +0100 Subject: [PATCH 06/21] chore: adjust types --- packages/common/src/invitationCode.ts | 44 +++++++----- packages/common/src/tests.ts | 34 ++++++++- .../JoinCommunity/JoinCommunity.test.tsx | 3 +- .../JoinCommunity/JoinCommunity.tsx | 23 ++++-- .../invitation/customProtocol.saga.test.ts | 10 +-- .../sagas/invitation/customProtocol.saga.ts | 62 +++++++++------- .../JoinCommunity/JoinCommunity.screen.tsx | 36 ++++++---- .../store/init/deepLink/deepLink.saga.test.ts | 21 +++--- .../src/store/init/deepLink/deepLink.saga.ts | 70 ++++++++++--------- .../invitationCode/invitationCode.ts | 11 ++- packages/types/src/network.ts | 13 +--- 11 files changed, 204 insertions(+), 123 deletions(-) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 38a90c0bb7..4ff43c86aa 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -201,22 +201,27 @@ export const composeInvitationDeepUrl = (data: InvitationData): string => { return composeInvitationUrl(`${DEEP_URL_SCHEME_WITH_SEPARATOR}`, data) } -const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => { +const composeInvitationUrl = (baseUrl: string, data: InvitationDataV1 | InvitationDataV2): string => { const url = new URL(baseUrl) - if (!data.version || data.version === InvitationDataVersion.v1) { - if (!data.pairs || !data.psk || !data.ownerOrbitDbIdentity) return '' // TODO: temporary until better solution is found - for (const pair of data.pairs) { - url.searchParams.append(pair.peerId, pair.onionAddress) - } - url.searchParams.append(PSK_PARAM_KEY, data.psk) - url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) - } else if (data.version === InvitationDataVersion.v2) { - if (!data.cid || !data.token || !data.serverAddress || !data.inviterAddress) return '' // TODO: temporary until better solution is found - url.searchParams.append(CID_PARAM_KEY, data.cid) - url.searchParams.append(TOKEN_PARAM_KEY, data.token) - url.searchParams.append(SERVER_ADDRESS_PARAM_KEY, data.serverAddress) - url.searchParams.append(INVITER_ADDRESS_PARAM_KEY, data.inviterAddress) + if (!data.version) data.version = InvitationDataVersion.v1 + + switch (data.version) { + case InvitationDataVersion.v1: + // if (!data.pairs || !data.psk || !data.ownerOrbitDbIdentity) return '' // TODO: temporary until better solution is found + for (const pair of data.pairs) { + url.searchParams.append(pair.peerId, pair.onionAddress) + } + url.searchParams.append(PSK_PARAM_KEY, data.psk) + url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) + break + case InvitationDataVersion.v2: + // if (!data.cid || !data.token || !data.serverAddress || !data.inviterAddress) return '' // TODO: temporary until better solution is found + url.searchParams.append(CID_PARAM_KEY, data.cid) + url.searchParams.append(TOKEN_PARAM_KEY, data.token) + url.searchParams.append(SERVER_ADDRESS_PARAM_KEY, data.serverAddress) + url.searchParams.append(INVITER_ADDRESS_PARAM_KEY, data.inviterAddress) + break } return url.href } @@ -233,10 +238,13 @@ export const argvInvitationCode = (argv: string[]): InvitationData | null => { } console.log('Parsing deep url', arg) invitationData = parseInvitationCodeDeepUrl(arg) - if (invitationData.pairs && invitationData.pairs.length > 0) { - break - } else { - invitationData = null + switch (invitationData.version) { + case InvitationDataVersion.v1: + if (invitationData.pairs.length > 0) { + break + } else { + invitationData = null + } } } return invitationData diff --git a/packages/common/src/tests.ts b/packages/common/src/tests.ts index 92acc606cf..5fd8e250de 100644 --- a/packages/common/src/tests.ts +++ b/packages/common/src/tests.ts @@ -1,8 +1,8 @@ -import { InvitationData } from '@quiet/types' +import { InvitationData, InvitationDataV1, InvitationDataV2, InvitationDataVersion } from '@quiet/types' import { composeInvitationDeepUrl, composeInvitationShareUrl } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' -export const validInvitationCodeTestData: InvitationData[] = [ +export const validInvitationDatav1: InvitationDataV1[] = [ { pairs: [ { @@ -25,7 +25,26 @@ export const validInvitationCodeTestData: InvitationData[] = [ }, ] -export const getValidInvitationUrlTestData = (data: InvitationData) => { +const validInvitationDatav2: InvitationDataV2[] = [ + { + version: InvitationDataVersion.v2, + cid: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPL', + token: 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw', + serverAddress: 'https://tryquiet.org/api/', + inviterAddress: 'pgzlcstu4ljvma7jqyalimcxlvss5bwlbba3c3iszgtwxee4qjdlgeqd', + }, +] + +export const validInvitationCodeTestData: InvitationData[] = [...validInvitationDatav1] + +type TestData = { + shareUrl: () => string + deepUrl: () => string + code: () => string + data: T +} + +export function getValidInvitationUrlTestData(data: T): TestData { return { shareUrl: () => composeInvitationShareUrl(data), deepUrl: () => composeInvitationDeepUrl(data), @@ -33,3 +52,12 @@ export const getValidInvitationUrlTestData = (data: InvitationData) => { data: data, } } + +// export const getValidInvitationUrlTestData = (data: InvitationData) => { +// return { +// shareUrl: () => composeInvitationShareUrl(data), +// deepUrl: () => composeInvitationDeepUrl(data), +// code: () => composeInvitationShareUrl(data).split(QUIET_JOIN_PAGE + '#')[1], +// data: data, +// } +// } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx index ee51e711e2..6d3d270fc8 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx @@ -23,10 +23,11 @@ import { validInvitationCodeTestData, getValidInvitationUrlTestData, PSK_PARAM_KEY, + validInvitationDatav1, } from '@quiet/common' describe('join community', () => { - const { code, data } = getValidInvitationUrlTestData(validInvitationCodeTestData[0]) + const { code, data } = getValidInvitationUrlTestData(validInvitationDatav1[0]) const validCode = code() diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index d4a63352ab..bed21050e8 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,7 +1,13 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationPair } from '@quiet/types' +import { + CommunityOwnership, + CreateNetworkPayload, + InvitationData, + InvitationDataVersion, + InvitationPair, +} from '@quiet/types' import { communities, identity, connection, network } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' import { ModalName } from '../../../sagas/modals/modals.types' @@ -39,13 +45,16 @@ const JoinCommunity = () => { }, [currentCommunity]) const handleCommunityAction = (data: InvitationData) => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + switch (data.version) { + case InvitationDataVersion.v1: + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + } + dispatch(communities.actions.createNetwork(payload)) } - dispatch(communities.actions.createNetwork(payload)) } // From 'You can create a new community instead' link diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts index 0a8402d75f..ddf7f45a5b 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts @@ -1,5 +1,5 @@ import { communities, getFactory, Store } from '@quiet/state-manager' -import { Community, CommunityOwnership, CreateNetworkPayload, InvitationData } from '@quiet/types' +import { Community, CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataV1 } from '@quiet/types' import { FactoryGirl } from 'factory-girl' import { expectSaga } from 'redux-saga-test-plan' import { customProtocolSaga } from './customProtocol.saga' @@ -8,13 +8,13 @@ import { prepareStore } from '../../testUtils/prepareStore' import { StoreKeys } from '../../store/store.keys' import { modalsActions } from '../modals/modals.slice' import { ModalName } from '../modals/modals.types' -import { validInvitationCodeTestData, getValidInvitationUrlTestData } from '@quiet/common' +import { getValidInvitationUrlTestData, validInvitationDatav1 } from '@quiet/common' describe('Handle invitation code', () => { let store: Store let factory: FactoryGirl let community: Community - let validInvitationData: InvitationData + let validInvitationData: InvitationDataV1 let validInvitationDeepUrl: string beforeEach(async () => { @@ -29,8 +29,8 @@ describe('Handle invitation code', () => { factory = await getFactory(store) - validInvitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).data - validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationCodeTestData[0]).deepUrl() + validInvitationData = getValidInvitationUrlTestData(validInvitationDatav1[0]).data + validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav1[0]).deepUrl() }) it('creates network if code is valid', async () => { diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 69c2c8f5c8..273441c701 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -55,26 +55,26 @@ export function* customProtocolSaga( // TODO: rename let isInvitationDataValid = false - if (!data.version || data.version === InvitationDataVersion.v1) { - const storedInvitationCodes = yield* select(communities.selectors.invitationCodes) - const currentInvitationCodes = data.pairs - - console.log('Stored invitation codes', storedInvitationCodes) - console.log('Current invitation codes', currentInvitationCodes) - - if (!currentInvitationCodes) { - isInvitationDataValid = false - } else if (storedInvitationCodes.length === 0) { + if (!data.version) data.version = InvitationDataVersion.v1 + + switch (data.version) { + case InvitationDataVersion.v1: + const storedPsk = yield* select(communities.selectors.psk) + const currentPsk = data.psk + + console.log('Stored psk', storedPsk) + console.log('Current psk', currentPsk) + + if (!currentPsk) { + isInvitationDataValid = false + } else if (!storedPsk) { + isInvitationDataValid = true + } else { + isInvitationDataValid = storedPsk === currentPsk + } + break + default: isInvitationDataValid = true - } else { - // TODO: check if psk is the same instead - isInvitationDataValid = storedInvitationCodes.some(storedCode => - currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) - ) - } - } else { - // TODO: ? - isInvitationDataValid = true } console.log('Is invitation data valid', isInvitationDataValid) @@ -135,11 +135,25 @@ export function* customProtocolSaga( return } - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + let payload: CreateNetworkPayload + + switch (data.version) { + case InvitationDataVersion.v1: + payload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + } + break + case InvitationDataVersion.v2: + // get data from the server + payload = { + ownership: CommunityOwnership.User, + peers: [], + psk: 'TODO', + ownerOrbitDbIdentity: 'TODO', + } } console.log('INIT_NAVIGATION: Creating network with payload', payload) yield* put(communities.actions.createNetwork(payload)) diff --git a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx index 381d6831af..4b49882037 100644 --- a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx +++ b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx @@ -2,7 +2,13 @@ import React, { FC, useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { identity, communities } from '@quiet/state-manager' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationPair } from '@quiet/types' +import { + CommunityOwnership, + CreateNetworkPayload, + InvitationData, + InvitationDataVersion, + InvitationPair, +} from '@quiet/types' import { JoinCommunity } from '../../components/JoinCommunity/JoinCommunity.component' import { navigationActions } from '../../store/navigation/navigation.slice' import { ScreenNames } from '../../const/ScreenNames.enum' @@ -36,18 +42,24 @@ export const JoinCommunityScreen: FC = ({ route }) => const joinCommunityAction = useCallback( (data: InvitationData) => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + // TODO: refactor or move to a saga + if (!data.version) data.version = InvitationDataVersion.v1 + switch (data.version) { + case InvitationDataVersion.v1: + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + } + dispatch(communities.actions.createNetwork(payload)) + dispatch( + navigationActions.navigation({ + screen: ScreenNames.UsernameRegistrationScreen, + }) + ) + break } - dispatch(communities.actions.createNetwork(payload)) - dispatch( - navigationActions.navigation({ - screen: ScreenNames.UsernameRegistrationScreen, - }) - ) }, [dispatch] ) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts index b019eb86c7..e84eab90aa 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -8,16 +8,21 @@ import { initActions } from '../init.slice' import { navigationActions } from '../../navigation/navigation.slice' import { ScreenNames } from '../../../const/ScreenNames.enum' import { deepLinkSaga } from './deepLink.saga' -import { type Community, CommunityOwnership, ConnectionProcessInfo, type Identity, InvitationData } from '@quiet/types' -import { composeInvitationShareUrl, validInvitationCodeTestData, getValidInvitationUrlTestData } from '@quiet/common' +import { type Community, CommunityOwnership, type Identity, InvitationData } from '@quiet/types' +import { + composeInvitationShareUrl, + validInvitationCodeTestData, + getValidInvitationUrlTestData, + validInvitationDatav1, +} from '@quiet/common' describe('deepLinkSaga', () => { let store: Store - const { code, data } = getValidInvitationUrlTestData(validInvitationCodeTestData[0]) + const { code } = getValidInvitationUrlTestData(validInvitationDatav1[0]) const validCode = code() - const validData = data + const validData = validInvitationDatav1[0] const id = '00d045ab' @@ -124,8 +129,8 @@ describe('deepLinkSaga', () => { ) // Store other communitys' invitation data in redux - const invitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[1]) - store.dispatch(communities.actions.setInvitationCodes(invitationData.data.pairs)) + // const invitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[1]) + store.dispatch(communities.actions.setInvitationCodes(validInvitationDatav1[0].pairs)) store.dispatch( communities.actions.addNewCommunity({ @@ -171,8 +176,8 @@ describe('deepLinkSaga', () => { store.dispatch(communities.actions.setCurrentCommunity(community.id)) - const invitationCodes = getInvitationCodes(validCode) - store.dispatch(communities.actions.setInvitationCodes(invitationCodes.pairs)) + // const invitationCodes = getInvitationCodes(validCode) + store.dispatch(communities.actions.setInvitationCodes(validData.pairs)) const reducer = combineReducers(reducers) await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index dce40fe8e0..b13a5c9c62 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -8,7 +8,6 @@ import { initActions } from '../init.slice' import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataVersion } from '@quiet/types' -import { areObjectsEqual } from '@quiet/common' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { const code = action.payload @@ -49,35 +48,28 @@ export function* deepLinkSaga(action: PayloadAction - currentInvitationCodes.some(currentCode => areObjectsEqual(storedCode, currentCode)) - ) - } - } else { - // TODO: ? - isInvitationDataValid = true } console.log('Is invitation data valid', isInvitationDataValid) @@ -142,11 +134,25 @@ export function* deepLinkSaga(action: PayloadAction { /** @@ -32,8 +32,13 @@ export const getInvitationCodes = (codeOrUrl: string): InvitationData => { data = parseInvitationCode(code) - if (!data || data?.pairs?.length === 0) { - throw new Error(`No invitation codes. Code/url passed: ${codeOrUrl}`) + if (!data.version) data.version = InvitationDataVersion.v1 + + switch (data.version) { + case InvitationDataVersion.v1: + if (data.pairs?.length === 0) { + throw new Error(`No invitation codes. Code/url passed: ${codeOrUrl}`) + } } return data diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index 2e8ce5bf2c..977bb063c0 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -14,25 +14,18 @@ export enum InvitationDataVersion { } export type InvitationDataV1 = { - version?: InvitationDataVersion + version?: InvitationDataVersion.v1 pairs: InvitationPair[] psk: string ownerOrbitDbIdentity: string } export type InvitationDataV2 = { - version?: InvitationDataVersion + version?: InvitationDataVersion.v2 cid: string token: string serverAddress: string inviterAddress: string } -// export type InvitationData = { -// version?: InvitationDataVersion -// pairs: InvitationPair[] -// psk: string -// ownerOrbitDbIdentity: string -// } - -export type InvitationData = Partial & Partial +export type InvitationData = InvitationDataV1 | InvitationDataV2 From d5f7b665d43f382fe740b8bd1ab2c8ea20a1ee84 Mon Sep 17 00:00:00 2001 From: Emi Date: Mon, 11 Mar 2024 15:18:09 +0100 Subject: [PATCH 07/21] chore: add joinNetwork saga that gathers data for createNetwork; mock DOWNLOAD_INVITE_DATA response --- .../backend/src/nest/socket/socket.service.ts | 17 +++++++ .../JoinCommunity/JoinCommunity.tsx | 11 +---- .../sagas/invitation/customProtocol.saga.ts | 33 ++++--------- .../JoinCommunity/JoinCommunity.screen.tsx | 24 +++------- .../src/store/init/deepLink/deepLink.saga.ts | 25 +--------- .../communities/communities.master.saga.ts | 2 + .../sagas/communities/communities.slice.ts | 1 + .../joinNetwork/joinNetwork.saga.ts | 46 +++++++++++++++++++ packages/state-manager/src/types.ts | 1 + packages/types/src/socket.ts | 1 + 10 files changed, 85 insertions(+), 76 deletions(-) create mode 100644 packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index 0057cc0e51..95086d5092 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -21,6 +21,8 @@ import { type DeleteChannelResponse, type MessagesLoadedPayload, type NetworkInfo, + CreateNetworkPayload, + CommunityOwnership, } from '@quiet/types' import EventEmitter from 'events' import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' @@ -170,6 +172,21 @@ export class SocketService extends EventEmitter implements OnModuleInit { } ) + socket.on( + SocketActionTypes.DOWNLOAD_INVITE_DATA, + async (payload: { serverAddress: string; cid: string }, callback: (response: CreateNetworkPayload) => void) => { + // this.emit(SocketActionTypes.DOWNLOAD_INVITE_DATA, payload, callback) + console.log('download invite data', payload) + // Mock it for now + callback({ + ownership: CommunityOwnership.User, + peers: [], + psk: '', + ownerOrbitDbIdentity: '', + }) + } + ) + socket.on(SocketActionTypes.LEAVE_COMMUNITY, async () => { this.logger('Leaving community') this.emit(SocketActionTypes.LEAVE_COMMUNITY) diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index bed21050e8..6ffe3431b5 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -45,16 +45,7 @@ const JoinCommunity = () => { }, [currentCommunity]) const handleCommunityAction = (data: InvitationData) => { - switch (data.version) { - case InvitationDataVersion.v1: - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, - } - dispatch(communities.actions.createNetwork(payload)) - } + dispatch(communities.actions.joinNetwork(data)) } // From 'You can create a new community instead' link diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 273441c701..4f9be3d3be 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -1,6 +1,12 @@ import { PayloadAction } from '@reduxjs/toolkit' -import { select, put, delay } from 'typed-redux-saga' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataVersion } from '@quiet/types' +import { select, put, delay, apply } from 'typed-redux-saga' +import { + CommunityOwnership, + CreateNetworkPayload, + InvitationData, + InvitationDataVersion, + SocketActionTypes, +} from '@quiet/types' import { communities, getInvitationCodes } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' import { ModalName } from '../modals/modals.types' @@ -135,26 +141,5 @@ export function* customProtocolSaga( return } - let payload: CreateNetworkPayload - - switch (data.version) { - case InvitationDataVersion.v1: - payload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, - } - break - case InvitationDataVersion.v2: - // get data from the server - payload = { - ownership: CommunityOwnership.User, - peers: [], - psk: 'TODO', - ownerOrbitDbIdentity: 'TODO', - } - } - console.log('INIT_NAVIGATION: Creating network with payload', payload) - yield* put(communities.actions.createNetwork(payload)) + yield* put(communities.actions.joinNetwork(data)) } diff --git a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx index 4b49882037..e7df1b76d1 100644 --- a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx +++ b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx @@ -42,24 +42,12 @@ export const JoinCommunityScreen: FC = ({ route }) => const joinCommunityAction = useCallback( (data: InvitationData) => { - // TODO: refactor or move to a saga - if (!data.version) data.version = InvitationDataVersion.v1 - switch (data.version) { - case InvitationDataVersion.v1: - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: data.pairs, - psk: data.psk, - ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, - } - dispatch(communities.actions.createNetwork(payload)) - dispatch( - navigationActions.navigation({ - screen: ScreenNames.UsernameRegistrationScreen, - }) - ) - break - } + dispatch(communities.actions.joinNetwork(data)) + dispatch( + navigationActions.navigation({ + screen: ScreenNames.UsernameRegistrationScreen, + }) + ) }, [dispatch] ) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index b13a5c9c62..ee59d820b4 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -134,30 +134,7 @@ export function* deepLinkSaga(action: PayloadAction state, sendCommunityMetadata: state => state, createNetwork: (state, _action: PayloadAction) => state, + joinNetwork: (state, _action: PayloadAction) => state, storePeerList: (state, action: PayloadAction) => { communitiesAdapter.updateOne(state.communities, { id: action.payload.communityId, diff --git a/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts new file mode 100644 index 0000000000..35cf4b0240 --- /dev/null +++ b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.ts @@ -0,0 +1,46 @@ +import { CommunityOwnership, CreateNetworkPayload, InvitationDataVersion, SocketActionTypes } from '@quiet/types' +import { PayloadAction } from '@reduxjs/toolkit' +import { apply, put } from 'typed-redux-saga' +import { Socket, applyEmitParams } from '../../../types' +import { communitiesActions } from '../communities.slice' + +export function* joinNetworkSaga( + socket: Socket, + action: PayloadAction['payload']> +) { + console.log('join network saga', action.payload) + const data = action.payload + let payload: CreateNetworkPayload + + data.version = data.version || InvitationDataVersion.v1 + switch (data.version) { + case InvitationDataVersion.v1: + console.log('join network saga invitation data v1') + payload = { + ownership: CommunityOwnership.User, + peers: data.pairs, + psk: data.psk, + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + } + break + case InvitationDataVersion.v2: + console.log('join network saga invitation data v2') + const response: CreateNetworkPayload = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.DOWNLOAD_INVITE_DATA, { + serverAddress: data.serverAddress, + cid: data.cid, + }) + ) + payload = { + ownership: CommunityOwnership.User, + peers: response.peers, + psk: response.psk, + ownerOrbitDbIdentity: response.ownerOrbitDbIdentity, + } + break + } + + yield* put(communitiesActions.createNetwork(payload)) +} diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index d356b88e2a..ea0506e604 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -54,6 +54,7 @@ export interface EmitEvents { [SocketActionTypes.SET_COMMUNITY_METADATA]: EmitEvent void> [SocketActionTypes.SET_COMMUNITY_CA_DATA]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent + [SocketActionTypes.DOWNLOAD_INVITE_DATA]: EmitEvent<{ serverAddress: string; cid: string }> } export type Socket = IOSocket diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 5888f41141..ee9575460d 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -14,6 +14,7 @@ export enum SocketActionTypes { COMMUNITY_LAUNCHED = 'communityLaunched', COMMUNITY_METADATA_STORED = 'communityMetadataStored', CREATE_COMMUNITY = 'createCommunity', + DOWNLOAD_INVITE_DATA = 'downloadInviteData', LAUNCH_COMMUNITY = 'launchCommunity', LEAVE_COMMUNITY = 'leaveCommunity', SET_COMMUNITY_CA_DATA = 'setCommunityCaData', From a4d674b44403e9d3b423d784ad0bf04f894fbba0 Mon Sep 17 00:00:00 2001 From: Emi Date: Thu, 14 Mar 2024 11:42:28 +0100 Subject: [PATCH 08/21] refactor: invitation link parsers --- packages/common/src/invitationCode.ts | 127 ++++++++++++++++---------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 4ff43c86aa..1fcfd016de 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,14 +1,15 @@ import { InvitationData, InvitationDataV1, InvitationDataV2, InvitationDataVersion, InvitationPair } from '@quiet/types' import { QUIET_JOIN_PAGE } from './static' import { createLibp2pAddress, isPSKcodeValid } from './libp2p' +// import { CID } from 'multiformats/cid' // Fixme: dependency issue import Logger from './logger' const logger = Logger('invite') -// V1 invitation code format (current) +// V1 invitation code format (p2p without relay) export const PSK_PARAM_KEY = 'k' export const OWNER_ORBIT_DB_IDENTITY_PARAM_KEY = 'o' -// V2 invitation code format (new) +// V2 invitation code format (relay support) export const CID_PARAM_KEY = 'c' export const TOKEN_PARAM_KEY = 't' export const INVITER_ADDRESS_PARAM_KEY = 'i' @@ -25,60 +26,33 @@ interface ParseDeepUrlParams { } const parseCodeV2 = (url: string): InvitationDataV2 => { + /** + * c=&t=&s=&i= + */ const params = new URL(url).searchParams + const requiredParams = [CID_PARAM_KEY, TOKEN_PARAM_KEY, SERVER_ADDRESS_PARAM_KEY, INVITER_ADDRESS_PARAM_KEY] - const cid = params.get(CID_PARAM_KEY) - if (!cid) throw new Error(`No cid found in invitation code '${url}'`) - // TODO: Validate CID format - params.delete(CID_PARAM_KEY) - - let token = params.get(TOKEN_PARAM_KEY) - if (!token) throw new Error(`No token found in invitation code '${url}'`) - token = decodeURIComponent(token) - // TODO: validate token format - params.delete(TOKEN_PARAM_KEY) - - let serverAddress = params.get(SERVER_ADDRESS_PARAM_KEY) - if (!serverAddress) throw new Error(`No server address found in invitation code '${url}'`) - serverAddress = decodeURIComponent(serverAddress) - try { - new URL(url) - } catch (e) { - throw new Error(`Invalid server address format '${url}'`) - } - params.delete(SERVER_ADDRESS_PARAM_KEY) - - let inviterAddress = params.get(INVITER_ADDRESS_PARAM_KEY) // TODO: can it be also peerId-onionAddress pair? - if (!inviterAddress) throw new Error(`No inviter address in invitation code '${url}'`) - inviterAddress = decodeURIComponent(inviterAddress) - if (!inviterAddress.trim().match(ONION_ADDRESS_REGEX)) { - throw new Error(`No inviter address in invitation code '${url}'`) - } - params.delete(INVITER_ADDRESS_PARAM_KEY) + const entries = validateUrlParams(params, requiredParams) return { version: InvitationDataVersion.v2, - cid, - token, - serverAddress, - inviterAddress, + cid: entries[CID_PARAM_KEY], + token: entries[TOKEN_PARAM_KEY], + serverAddress: entries[SERVER_ADDRESS_PARAM_KEY], + inviterAddress: entries[INVITER_ADDRESS_PARAM_KEY], } } const parseCodeV1 = (url: string): InvitationDataV1 => { + /** + * =&=...&k=&o= + */ const params = new URL(url).searchParams + const requiredParams = [PSK_PARAM_KEY, OWNER_ORBIT_DB_IDENTITY_PARAM_KEY] - let psk = params.get(PSK_PARAM_KEY) - const codes: InvitationPair[] = [] - if (!psk) throw new Error(`No psk found in invitation code '${url}'`) - psk = decodeURIComponent(psk) - if (!isPSKcodeValid(psk)) throw new Error(`Invalid psk in invitation code '${url}'`) - params.delete(PSK_PARAM_KEY) + const entries = validateUrlParams(params, requiredParams) - let ownerOrbitDbIdentity = params.get(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY) - if (!ownerOrbitDbIdentity) throw new Error(`No owner OrbitDB identity found in invitation code '${url}'`) - ownerOrbitDbIdentity = decodeURIComponent(ownerOrbitDbIdentity) - params.delete(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY) + const codes: InvitationPair[] = [] params.forEach((onionAddress, peerId) => { if (!peerDataValid({ peerId, onionAddress })) return @@ -87,11 +61,14 @@ const parseCodeV1 = (url: string): InvitationDataV1 => { onionAddress, }) }) + + if (codes.length === 0) throw new Error(`No valid peer addresses found in invitation code '${url}'`) + return { version: InvitationDataVersion.v1, pairs: codes, - psk, - ownerOrbitDbIdentity, + psk: entries[PSK_PARAM_KEY], + ownerOrbitDbIdentity: entries[OWNER_ORBIT_DB_IDENTITY_PARAM_KEY], } } @@ -208,7 +185,6 @@ const composeInvitationUrl = (baseUrl: string, data: InvitationDataV1 | Invitati switch (data.version) { case InvitationDataVersion.v1: - // if (!data.pairs || !data.psk || !data.ownerOrbitDbIdentity) return '' // TODO: temporary until better solution is found for (const pair of data.pairs) { url.searchParams.append(pair.peerId, pair.onionAddress) } @@ -216,7 +192,6 @@ const composeInvitationUrl = (baseUrl: string, data: InvitationDataV1 | Invitati url.searchParams.append(OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity) break case InvitationDataVersion.v2: - // if (!data.cid || !data.token || !data.serverAddress || !data.inviterAddress) return '' // TODO: temporary until better solution is found url.searchParams.append(CID_PARAM_KEY, data.cid) url.searchParams.append(TOKEN_PARAM_KEY, data.token) url.searchParams.append(SERVER_ADDRESS_PARAM_KEY, data.serverAddress) @@ -262,3 +237,59 @@ const peerDataValid = ({ peerId, onionAddress }: { peerId: string; onionAddress: } return true } + +const validateUrlParams = (params: URLSearchParams, requiredParams: string[]) => { + const entries = Object.fromEntries(params) + + requiredParams.forEach(key => { + const value = params.get(key) + if (!value) { + throw new Error(`Missing key '${key}' in invitation code`) + } + entries[key] = decodeURIComponent(value) + if (!isParamValid(key, entries[key])) { + throw new Error(`Invalid value '${value}' for key '${key}' in invitation code`) + } + params.delete(key) + }) + return entries +} + +const isParamValid = (param: string, value: string) => { + logger(`Validating param ${param} with value ${value}`) + switch (param) { + case CID_PARAM_KEY: + // try { + // CID.parse(value) + // } catch (e) { + // logger.error(e.message) + // return false + // } + return true + + case TOKEN_PARAM_KEY: + // TODO: validate token format + return true + + case SERVER_ADDRESS_PARAM_KEY: + try { + new URL(value) + } catch (e) { + logger.error(e.message) + return false + } + break + + case INVITER_ADDRESS_PARAM_KEY: + return Boolean(value.trim().match(ONION_ADDRESS_REGEX)) + + case PSK_PARAM_KEY: + return isPSKcodeValid(value) + + case OWNER_ORBIT_DB_IDENTITY_PARAM_KEY: + return true + + default: + return false + } +} From 8cc6fd3df1539b98bcb3c3e0ceddc8a9dad73c68 Mon Sep 17 00:00:00 2001 From: Emi Date: Thu, 14 Mar 2024 17:35:22 +0100 Subject: [PATCH 09/21] fix: deepLink saga tests --- .../store/init/deepLink/deepLink.saga.test.ts | 71 +++---------------- 1 file changed, 11 insertions(+), 60 deletions(-) diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts index e84eab90aa..862a3a8339 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -8,7 +8,7 @@ import { initActions } from '../init.slice' import { navigationActions } from '../../navigation/navigation.slice' import { ScreenNames } from '../../../const/ScreenNames.enum' import { deepLinkSaga } from './deepLink.saga' -import { type Community, CommunityOwnership, type Identity, InvitationData } from '@quiet/types' +import { type Community, CommunityOwnership, type Identity, InvitationData, InvitationDataVersion } from '@quiet/types' import { composeInvitationShareUrl, validInvitationCodeTestData, @@ -66,11 +66,9 @@ describe('deepLinkSaga', () => { .withState(store.getState()) .put(initActions.resetDeepLink()) .put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, + communities.actions.joinNetwork({ + version: InvitationDataVersion.v1, + ...validData, }) ) .put( @@ -81,45 +79,6 @@ describe('deepLinkSaga', () => { .run() }) - // FIXME: Currently there's no way to actually check whether the redirection destionation is correct - test.skip('opens channel list screen if the same url has been used', async () => { - store.dispatch( - initActions.setWebsocketConnected({ - dataPort: 5001, - socketIOSecret: 'secret', - }) - ) - - store.dispatch(communities.actions.setInvitationCodes(validData.pairs)) - store.dispatch( - communities.actions.addNewCommunity({ - ...community, - name: 'rockets', - }) - ) - - store.dispatch( - // @ts-expect-error - identity.actions.addNewIdentity({ ..._identity, userCertificate: 'certificate' }) - ) - - store.dispatch(communities.actions.setCurrentCommunity(community.id)) - - const reducer = combineReducers(reducers) - await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) - .withReducer(reducer) - .withState(store.getState()) - .not.put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, - }) - ) - .run() - }) - test('displays error if user already belongs to a community', async () => { store.dispatch( initActions.setWebsocketConnected({ @@ -128,10 +87,6 @@ describe('deepLinkSaga', () => { }) ) - // Store other communitys' invitation data in redux - // const invitationData = getValidInvitationUrlTestData(validInvitationCodeTestData[1]) - store.dispatch(communities.actions.setInvitationCodes(validInvitationDatav1[0].pairs)) - store.dispatch( communities.actions.addNewCommunity({ ...community, @@ -176,8 +131,7 @@ describe('deepLinkSaga', () => { store.dispatch(communities.actions.setCurrentCommunity(community.id)) - // const invitationCodes = getInvitationCodes(validCode) - store.dispatch(communities.actions.setInvitationCodes(validData.pairs)) + store.dispatch(communities.actions.savePSK(validData.psk)) const reducer = combineReducers(reducers) await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) @@ -197,11 +151,10 @@ describe('deepLinkSaga', () => { }) .put.like({ action: { - type: communities.actions.createNetwork.type, + type: communities.actions.joinNetwork.type, payload: { - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, + version: InvitationDataVersion.v1, + ...validData, }, }, }) @@ -243,11 +196,9 @@ describe('deepLinkSaga', () => { }, }) .not.put( - communities.actions.createNetwork({ - ownership: CommunityOwnership.User, - peers: validData.pairs, - psk: validData.psk, - ownerOrbitDbIdentity: validData.ownerOrbitDbIdentity, + communities.actions.joinNetwork({ + version: InvitationDataVersion.v1, + ...validData, }) ) .run() From b8203f925913d7485de5db6a6818ae3690c0d072 Mon Sep 17 00:00:00 2001 From: Emi Date: Fri, 15 Mar 2024 16:36:05 +0100 Subject: [PATCH 10/21] refactor: simplify deepLink and customProtocol sagas --- .../JoinCommunity/JoinCommunity.tsx | 14 ++--- .../PerformCommunityActionComponent.tsx | 2 + .../sagas/invitation/customProtocol.saga.ts | 55 +++---------------- .../JoinCommunity/JoinCommunity.screen.tsx | 8 +-- .../src/store/init/deepLink/deepLink.saga.ts | 42 ++------------ .../joinNetwork/joinNetwork.saga.ts | 1 + .../launchCommunity/launchCommunity.saga.ts | 1 + .../invitationCode/invitationCode.ts | 16 +----- 8 files changed, 24 insertions(+), 115 deletions(-) diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index 6ffe3431b5..f8d5b899b8 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,17 +1,11 @@ +import { communities, connection, identity } from '@quiet/state-manager' +import { CommunityOwnership, InvitationData } from '@quiet/types' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { - CommunityOwnership, - CreateNetworkPayload, - InvitationData, - InvitationDataVersion, - InvitationPair, -} from '@quiet/types' -import { communities, identity, connection, network } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' -import { ModalName } from '../../../sagas/modals/modals.types' import { useModal } from '../../../containers/hooks' +import { ModalName } from '../../../sagas/modals/modals.types' +import { socketSelectors } from '../../../sagas/socket/socket.selectors' const JoinCommunity = () => { const dispatch = useDispatch() diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index 754bb453f4..6d815bf958 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -224,6 +224,8 @@ export const PerformCommunityActionComponent: React.FC { if (communityOwnership === CommunityOwnership.User && invitationCode?.length && psk && ownerOrbitDbIdentity) { setFormSent(true) diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts index 4f9be3d3be..0e889f7ed1 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.ts @@ -1,17 +1,11 @@ import { PayloadAction } from '@reduxjs/toolkit' -import { select, put, delay, apply } from 'typed-redux-saga' -import { - CommunityOwnership, - CreateNetworkPayload, - InvitationData, - InvitationDataVersion, - SocketActionTypes, -} from '@quiet/types' -import { communities, getInvitationCodes } from '@quiet/state-manager' +import { select, put, delay } from 'typed-redux-saga' +import { InvitationData, InvitationDataVersion } from '@quiet/types' +import { communities } from '@quiet/state-manager' import { socketSelectors } from '../socket/socket.selectors' import { ModalName } from '../modals/modals.types' import { modalsActions } from '../modals/modals.slice' -import { areObjectsEqual, argvInvitationCode } from '@quiet/common' +import { argvInvitationCode } from '@quiet/common' export function* customProtocolSaga( action: PayloadAction['payload']> @@ -58,10 +52,7 @@ export function* customProtocolSaga( const community = yield* select(communities.selectors.currentCommunity) - // TODO: rename - let isInvitationDataValid = false - - if (!data.version) data.version = InvitationDataVersion.v1 + let isJoiningAnotherCommunity = false switch (data.version) { case InvitationDataVersion.v1: @@ -71,45 +62,15 @@ export function* customProtocolSaga( console.log('Stored psk', storedPsk) console.log('Current psk', currentPsk) - if (!currentPsk) { - isInvitationDataValid = false - } else if (!storedPsk) { - isInvitationDataValid = true - } else { - isInvitationDataValid = storedPsk === currentPsk - } + isJoiningAnotherCommunity = Boolean(storedPsk && storedPsk !== currentPsk) break - default: - isInvitationDataValid = true } - console.log('Is invitation data valid', isInvitationDataValid) - const isAlreadyConnected = Boolean(community?.name) - - const alreadyBelongsWithAnotherCommunity = !isInvitationDataValid && isAlreadyConnected - const connectingWithAnotherCommunity = !isInvitationDataValid && !isAlreadyConnected - const alreadyBelongsWithCurrentCommunity = isInvitationDataValid && isAlreadyConnected - const connectingWithCurrentCommunity = isInvitationDataValid && !isAlreadyConnected - - if (alreadyBelongsWithAnotherCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Already belongs with another community.') - } - - if (connectingWithAnotherCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Proceeding with connection to another community.') - } - - if (alreadyBelongsWithCurrentCommunity) { - console.log('INIT_NAVIGATION: ABORTING: Already connected with the current community.') - } - - if (connectingWithCurrentCommunity) { - console.log('INIT_NAVIGATION: Proceeding with connection to the community.') - } + const connectingWithAnotherCommunity = isJoiningAnotherCommunity && !isAlreadyConnected // User already belongs to a community - if (alreadyBelongsWithAnotherCommunity || alreadyBelongsWithCurrentCommunity) { + if (isAlreadyConnected) { console.log('INIT_NAVIGATION: Displaying error (user already belongs to a community).') yield* put(communities.actions.clearInvitationCodes()) // TODO: check out yield* put( diff --git a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx index e7df1b76d1..f992945a03 100644 --- a/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx +++ b/packages/mobile/src/screens/JoinCommunity/JoinCommunity.screen.tsx @@ -2,13 +2,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { identity, communities } from '@quiet/state-manager' -import { - CommunityOwnership, - CreateNetworkPayload, - InvitationData, - InvitationDataVersion, - InvitationPair, -} from '@quiet/types' +import { InvitationData } from '@quiet/types' import { JoinCommunity } from '../../components/JoinCommunity/JoinCommunity.component' import { navigationActions } from '../../store/navigation/navigation.slice' import { ScreenNames } from '../../const/ScreenNames.enum' diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts index ee59d820b4..28d7a03fc8 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.ts @@ -7,7 +7,7 @@ import { initSelectors } from '../init.selectors' import { initActions } from '../init.slice' import { appImages } from '../../../assets' import { replaceScreen } from '../../../RootNavigation' -import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationDataVersion } from '@quiet/types' +import { InvitationData, InvitationDataVersion } from '@quiet/types' export function* deepLinkSaga(action: PayloadAction['payload']>): Generator { const code = action.payload @@ -48,9 +48,7 @@ export function* deepLinkSaga(action: PayloadAction['payload']> ): Generator { + console.log('LAUNCH COMMUNITY SAGA') const communityId = action.payload const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts index 49efa492f3..e147a2fea8 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.ts @@ -1,12 +1,11 @@ import { Site, parseInvitationCode } from '@quiet/common' -import { InvitationDataVersion, type InvitationData } from '@quiet/types' +import { type InvitationData } from '@quiet/types' export const getInvitationCodes = (codeOrUrl: string): InvitationData => { /** * Extract codes from invitation share url or return passed value for further error handling * @param codeOrUrl: full invitation link or just the code part of the link */ - let data: InvitationData | null = null let potentialCode let validUrl: URL | null = null @@ -30,16 +29,5 @@ export const getInvitationCodes = (codeOrUrl: string): InvitationData => { code = potentialCode } - data = parseInvitationCode(code) - - if (!data.version) data.version = InvitationDataVersion.v1 - - switch (data.version) { - case InvitationDataVersion.v1: - if (data.pairs?.length === 0) { - throw new Error(`No invitation codes. Code/url passed: ${codeOrUrl}`) - } - } - - return data + return parseInvitationCode(code) } From 7d75c3a1d0f48d6fc59068f32a47076269b43452 Mon Sep 17 00:00:00 2001 From: Emi Date: Fri, 15 Mar 2024 20:50:26 +0100 Subject: [PATCH 11/21] fix: invitation code utils tests --- packages/common/src/invitationCode.test.ts | 104 +++++++++++++++++---- packages/common/src/invitationCode.ts | 8 +- packages/common/src/tests.ts | 4 +- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 91eaa5d55d..2403312823 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,15 +1,22 @@ -import { InvitationData, InvitationPair } from '@quiet/types' +import { InvitationData, InvitationDataVersion, InvitationPair } from '@quiet/types' import { argvInvitationCode, composeInvitationDeepUrl, composeInvitationShareUrl, parseInvitationCodeDeepUrl, PSK_PARAM_KEY, + OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, p2pAddressesToPairs, + CID_PARAM_KEY, + TOKEN_PARAM_KEY, + SERVER_ADDRESS_PARAM_KEY, + INVITER_ADDRESS_PARAM_KEY, + DEEP_URL_SCHEME_WITH_SEPARATOR, } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' +import { validInvitationDatav2 } from './tests' -describe('Invitation code helper', () => { +describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' @@ -31,24 +38,22 @@ describe('Invitation code helper', () => { 'something', 'quiet:/invalid', 'zbay://invalid', - 'quiet://invalid', - 'quiet://?param=invalid', composeInvitationDeepUrl(expectedCodes), ]) expect(result).toEqual(expectedCodes) }) - it('returns null if argv do not contain any valid invitation code', () => { - const result = argvInvitationCode([ - 'something', - 'quiet:/invalid', - 'zbay://invalid', - 'quiet://invalid', - 'quiet://?param=invalid', - ]) + it('returns null if argv do not contain any url with proper scheme', () => { + const result = argvInvitationCode(['something', 'quiet:/invalid', 'zbay://invalid']) expect(result).toBeNull() }) + it('throws error if argv contains invalid invitation url', () => { + expect(() => { + argvInvitationCode(['something', 'quiet:/invalid', 'quiet://?param=invalid']) + }).toThrow() + }) + it('composes proper invitation deep url', () => { expect( composeInvitationDeepUrl({ @@ -59,7 +64,9 @@ describe('Invitation code helper', () => { psk: pskDecoded, ownerOrbitDbIdentity, }) - ).toEqual(`quiet://?peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}`) + ).toEqual( + `quiet://?peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` + ) }) it('creates invitation share url based on invitation data', () => { @@ -71,7 +78,7 @@ describe('Invitation code helper', () => { psk: pskDecoded, ownerOrbitDbIdentity, } - const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}` + const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` expect(composeInvitationShareUrl(pairs)).toEqual(expected) }) @@ -85,14 +92,16 @@ describe('Invitation code helper', () => { 'invalidAddress', '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', ] + console.log('p2pAddressesToPairs(peerList)', p2pAddressesToPairs(peerList)) expect(p2pAddressesToPairs(peerList)).toEqual([pair]) }) it('retrieves invitation codes from deep url', () => { const codes = parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` + `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` ) expect(codes).toEqual({ + version: InvitationDataVersion.v1, pairs: [ { peerId: peerId1, onionAddress: address1 }, { peerId: peerId2, onionAddress: address2 }, @@ -106,7 +115,9 @@ describe('Invitation code helper', () => { 'parsing invitation code throws error if psk is invalid: (%s)', (psk: string) => { expect(() => { - parseInvitationCodeDeepUrl(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}`) + parseInvitationCodeDeepUrl( + `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` + ) }).toThrow() } ) @@ -115,8 +126,65 @@ describe('Invitation code helper', () => { const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs' const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' const parsed = parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` + `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` ) - expect(parsed).toEqual({ pairs: [{ peerId: peerId1, onionAddress: address1 }], psk: pskDecoded }) + expect(parsed).toEqual({ + version: InvitationDataVersion.v1, + pairs: [{ peerId: peerId1, onionAddress: address1 }], + psk: pskDecoded, + ownerOrbitDbIdentity, + }) + }) +}) + +describe(`Invitation code helper ${InvitationDataVersion.v2}`, () => { + const data = validInvitationDatav2[0] + const urlParams = [ + [CID_PARAM_KEY, data.cid], + [TOKEN_PARAM_KEY, data.token], + [SERVER_ADDRESS_PARAM_KEY, data.serverAddress], + [INVITER_ADDRESS_PARAM_KEY, data.inviterAddress], + ] + + it('creates invitation share url based on invitation data', () => { + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationShareUrl(data)).toEqual(url.href.replace('?', '#')) + }) + + it('composes proper invitation deep url', () => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationDeepUrl(data)).toEqual(url.href) + }) + + it('retrieves invitation codes from deep url v2', () => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const codes = parseInvitationCodeDeepUrl(url.href) + expect(codes).toEqual({ + version: InvitationDataVersion.v2, + cid: data.cid, + token: data.token, + serverAddress: data.serverAddress, + inviterAddress: data.inviterAddress, + }) + }) + + it.each([ + // TODO: add check for invalid token + [CID_PARAM_KEY, 'sth'], + [SERVER_ADDRESS_PARAM_KEY, 'website.com'], + [INVITER_ADDRESS_PARAM_KEY, 'abcd'], + ])('parsing deep url throws error if data is invalid: %s=%s', (paramKey: string, paramValue: string) => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + // Replace valid param value with invalid one + url.searchParams.set(paramKey, paramValue) + + expect(() => { + parseInvitationCodeDeepUrl(url.href) + }).toThrow() }) }) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 1fcfd016de..1b748a70e2 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -15,7 +15,7 @@ export const TOKEN_PARAM_KEY = 't' export const INVITER_ADDRESS_PARAM_KEY = 'i' export const SERVER_ADDRESS_PARAM_KEY = 's' -const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' +export const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' const DEEP_URL_SCHEME = 'quiet' const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g const PEER_ID_REGEX = /^[a-zA-Z0-9]{46}$/g @@ -153,6 +153,8 @@ export const p2pAddressesToPairs = (addresses: string[]): InvitationPair[] => { continue } const rawAddress = onionAddress.endsWith('.onion') ? onionAddress.split('.')[0] : onionAddress + if (!peerDataValid({ peerId, onionAddress: rawAddress })) continue + pairs.push({ peerId: peerId, onionAddress: rawAddress }) } return pairs @@ -265,7 +267,7 @@ const isParamValid = (param: string, value: string) => { // logger.error(e.message) // return false // } - return true + return Boolean(value.match(PEER_ID_REGEX)) case TOKEN_PARAM_KEY: // TODO: validate token format @@ -278,7 +280,7 @@ const isParamValid = (param: string, value: string) => { logger.error(e.message) return false } - break + return true case INVITER_ADDRESS_PARAM_KEY: return Boolean(value.trim().match(ONION_ADDRESS_REGEX)) diff --git a/packages/common/src/tests.ts b/packages/common/src/tests.ts index 5fd8e250de..6c4791f4bb 100644 --- a/packages/common/src/tests.ts +++ b/packages/common/src/tests.ts @@ -25,10 +25,10 @@ export const validInvitationDatav1: InvitationDataV1[] = [ }, ] -const validInvitationDatav2: InvitationDataV2[] = [ +export const validInvitationDatav2: InvitationDataV2[] = [ { version: InvitationDataVersion.v2, - cid: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPL', + cid: 'QmaRchXhkPWq8iLiMZwFfd2Yi4iESWhAYYJt8cTCVXSwpG', token: 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw', serverAddress: 'https://tryquiet.org/api/', inviterAddress: 'pgzlcstu4ljvma7jqyalimcxlvss5bwlbba3c3iszgtwxee4qjdlgeqd', From d333cfea0c14ea53beca9386b65f274837a41cbe Mon Sep 17 00:00:00 2001 From: Emi Date: Fri, 15 Mar 2024 20:51:32 +0100 Subject: [PATCH 12/21] chore: add missing github workflow for running common package tests --- .github/workflows/utils-tests.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/utils-tests.yml diff --git a/.github/workflows/utils-tests.yml b/.github/workflows/utils-tests.yml new file mode 100644 index 0000000000..15edc17f60 --- /dev/null +++ b/.github/workflows/utils-tests.yml @@ -0,0 +1,29 @@ +name: Common package tests + +on: + pull_request: + paths: + - packages/common/** + +jobs: + utils-tests: + timeout-minutes: 25 + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-20.04, macos-latest, windows-2019] + + steps: + - name: "Print OS" + run: echo ${{ matrix.os }} + + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: "Setup environment" + uses: ./.github/actions/setup-env + with: + bootstrap-packages: "@quiet/eslint-config,@quiet/logger,@quiet/types,@quiet/common" + + - name: "Unit tests" + run: lerna run test --scope @quiet/common --stream From de8533cf12f3ce1f56f075b3c21212797bea48cc Mon Sep 17 00:00:00 2001 From: Emi Date: Mon, 18 Mar 2024 15:21:56 +0100 Subject: [PATCH 13/21] refactor: invitation code tests --- packages/common/src/invitationCode.test.ts | 134 +++++++++------------ packages/common/src/invitationCode.ts | 1 + 2 files changed, 59 insertions(+), 76 deletions(-) diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 2403312823..f35582e35d 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,4 +1,4 @@ -import { InvitationData, InvitationDataVersion, InvitationPair } from '@quiet/types' +import { InvitationDataV1, InvitationDataVersion, InvitationPair } from '@quiet/types' import { argvInvitationCode, composeInvitationDeepUrl, @@ -14,33 +14,26 @@ import { DEEP_URL_SCHEME_WITH_SEPARATOR, } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' -import { validInvitationDatav2 } from './tests' +import { validInvitationDatav1, validInvitationDatav2 } from './tests' +import { createLibp2pAddress } from './libp2p' describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { - const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const psk = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw%3D' - const pskDecoded = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=' - const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity' + const data: InvitationDataV1 = { + ...validInvitationDatav1[0], + pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId2, onionAddress: address1 }], + } + const urlParams = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] it('retrieves invitation code from argv', () => { - const expectedCodes: InvitationData = { - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - } - const result = argvInvitationCode([ - 'something', - 'quiet:/invalid', - 'zbay://invalid', - composeInvitationDeepUrl(expectedCodes), - ]) - expect(result).toEqual(expectedCodes) + const result = argvInvitationCode(['something', 'quiet:/invalid', 'zbay://invalid', composeInvitationDeepUrl(data)]) + expect(result).toEqual(data) }) it('returns null if argv do not contain any url with proper scheme', () => { @@ -55,31 +48,15 @@ describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { }) it('composes proper invitation deep url', () => { - expect( - composeInvitationDeepUrl({ - pairs: [ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - }) - ).toEqual( - `quiet://?peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` - ) + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationDeepUrl(data)).toEqual(url.href) }) it('creates invitation share url based on invitation data', () => { - const pairs: InvitationData = { - pairs: [ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, - } - const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` - expect(composeInvitationShareUrl(pairs)).toEqual(expected) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + expect(composeInvitationShareUrl(data)).toEqual(url.href.replace('?', '#')) }) it('converts list of p2p addresses to invitation pairs', () => { @@ -88,51 +65,56 @@ describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { onionAddress: 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad', } const peerList = [ - `/dns4/${pair.onionAddress}.onion/tcp/443/wss/p2p/${pair.peerId}`, + createLibp2pAddress(pair.onionAddress, pair.peerId), 'invalidAddress', - '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', + createLibp2pAddress('somethingElse.onion', 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'), ] - console.log('p2pAddressesToPairs(peerList)', p2pAddressesToPairs(peerList)) expect(p2pAddressesToPairs(peerList)).toEqual([pair]) }) it('retrieves invitation codes from deep url', () => { - const codes = parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` - ) + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + const codes = parseInvitationCodeDeepUrl(url.href) expect(codes).toEqual({ version: InvitationDataVersion.v1, - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk: pskDecoded, - ownerOrbitDbIdentity, + ...data, }) }) - it.each([['12345'], ['a2FzemE='], 'a2FycGllIHcgZ2FsYXJlY2llIGVjaWUgcGVjaWUgYWxlIGkgdGFrIHpqZWNpZQ=='])( - 'parsing invitation code throws error if psk is invalid: (%s)', - (psk: string) => { - expect(() => { - parseInvitationCodeDeepUrl( - `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` - ) - }).toThrow() - } - ) - - it('retrieves invitation codes from deep url with partly invalid codes', () => { - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const parsed = parseInvitationCodeDeepUrl( - `${DEEP_URL_SCHEME_WITH_SEPARATOR}?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${ownerOrbitDbIdentity}` - ) + it.each([ + [PSK_PARAM_KEY, '12345'], + [PSK_PARAM_KEY, 'a2FzemE='], + [PSK_PARAM_KEY, 'a2FycGllIHcgZ2FsYXJlY2llIGVjaWUgcGVjaWUgYWxlIGkgdGFrIHpqZWNpZQ=='], + ])('parsing deep url throws error if data is invalid: %s=%s', (paramKey: string, paramValue: string) => { + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + + // Replace valid param value with invalid one + url.searchParams.set(paramKey, paramValue) + + expect(() => { + parseInvitationCodeDeepUrl(url.href) + }).toThrow() + }) + + it('retrieves invitation codes from deep url with partly invalid addresses', () => { + const urlParamsWithInvalidAddress = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wf', 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdv'], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] + + const url = new URL(DEEP_URL_SCHEME_WITH_SEPARATOR) + urlParamsWithInvalidAddress.forEach(([key, value]) => url.searchParams.append(key, value)) + + const parsed = parseInvitationCodeDeepUrl(url.href) expect(parsed).toEqual({ version: InvitationDataVersion.v1, - pairs: [{ peerId: peerId1, onionAddress: address1 }], - psk: pskDecoded, - ownerOrbitDbIdentity, + ...data, }) }) }) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index 1b748a70e2..16182ed325 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -289,6 +289,7 @@ const isParamValid = (param: string, value: string) => { return isPSKcodeValid(value) case OWNER_ORBIT_DB_IDENTITY_PARAM_KEY: + // TODO: validate orbit db identity format? return true default: From 4895b2953e5747f6c2a8794caf967c1014c59508 Mon Sep 17 00:00:00 2001 From: Emi Date: Mon, 18 Mar 2024 17:40:03 +0100 Subject: [PATCH 14/21] fix: unit tests --- packages/common/src/invitationCode.test.ts | 6 +- .../invitation/customProtocol.saga.test.ts | 21 ++-- .../src/rtl-tests/community.join.test.tsx | 1 + .../src/rtl-tests/deep.linking.test.tsx | 1 + .../mobile/src/tests/deep.linking.test.tsx | 3 +- .../joinNetwork/joinNetwork.saga.test.ts | 45 +++++++++ .../invitationCode/invitationCode.test.ts | 98 +++++++++++-------- 7 files changed, 121 insertions(+), 54 deletions(-) create mode 100644 packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index f35582e35d..6a48b409a2 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -18,11 +18,11 @@ import { validInvitationDatav1, validInvitationDatav2 } from './tests' import { createLibp2pAddress } from './libp2p' describe(`Invitation code helper ${InvitationDataVersion.v1}`, () => { - const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const address = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' const data: InvitationDataV1 = { ...validInvitationDatav1[0], - pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId2, onionAddress: address1 }], + pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId, onionAddress: address }], } const urlParams = [ [data.pairs[0].peerId, data.pairs[0].onionAddress], diff --git a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts index ddf7f45a5b..c97078e808 100644 --- a/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts +++ b/packages/desktop/src/renderer/sagas/invitation/customProtocol.saga.test.ts @@ -8,7 +8,7 @@ import { prepareStore } from '../../testUtils/prepareStore' import { StoreKeys } from '../../store/store.keys' import { modalsActions } from '../modals/modals.slice' import { ModalName } from '../modals/modals.types' -import { getValidInvitationUrlTestData, validInvitationDatav1 } from '@quiet/common' +import { getValidInvitationUrlTestData, validInvitationDatav1, validInvitationDatav2 } from '@quiet/common' describe('Handle invitation code', () => { let store: Store @@ -33,16 +33,19 @@ describe('Handle invitation code', () => { validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav1[0]).deepUrl() }) - it('creates network if code is valid', async () => { - const payload: CreateNetworkPayload = { - ownership: CommunityOwnership.User, - peers: validInvitationData.pairs, - psk: validInvitationData.psk, - ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, - } + it('joins network if code is valid', async () => { + await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) + .withState(store.getState()) + .put(communities.actions.joinNetwork(validInvitationData)) + .run() + }) + + it('joins network if v2 code is valid', async () => { + const validInvitationData = getValidInvitationUrlTestData(validInvitationDatav2[0]).data + const validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav2[0]).deepUrl() await expectSaga(customProtocolSaga, communities.actions.customProtocol([validInvitationDeepUrl])) .withState(store.getState()) - .put(communities.actions.createNetwork(payload)) + .put(communities.actions.joinNetwork(validInvitationData)) .run() }) diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index 880ad07f1c..1a945aa070 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -166,6 +166,7 @@ describe('User', () => { expect(actions).toMatchInlineSnapshot(` Array [ + "Communities/joinNetwork", "Communities/createNetwork", "Communities/setInvitationCodes", "Communities/savePSK", diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index 464b9ad6e6..2e0abbc878 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -54,6 +54,7 @@ describe('Deep linking', () => { expect(actions).toMatchInlineSnapshot(` Array [ "Communities/customProtocol", + "Communities/joinNetwork", "Communities/createNetwork", "Communities/setInvitationCodes", "Communities/savePSK", diff --git a/packages/mobile/src/tests/deep.linking.test.tsx b/packages/mobile/src/tests/deep.linking.test.tsx index 7bcc553f93..6a0156cbbb 100644 --- a/packages/mobile/src/tests/deep.linking.test.tsx +++ b/packages/mobile/src/tests/deep.linking.test.tsx @@ -50,9 +50,10 @@ describe('Deep linking', () => { [ "Init/deepLink", "Init/resetDeepLink", + "Communities/joinNetwork", "Communities/createNetwork", - "Communities/setInvitationCodes", "Navigation/replaceScreen", + "Communities/setInvitationCodes", "Communities/savePSK", "Communities/addNewCommunity", "Communities/setCurrentCommunity", diff --git a/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts new file mode 100644 index 0000000000..151d16cf0b --- /dev/null +++ b/packages/state-manager/src/sagas/communities/joinNetwork/joinNetwork.saga.test.ts @@ -0,0 +1,45 @@ +import { getValidInvitationUrlTestData, validInvitationDatav1 } from '@quiet/common' +import { CommunityOwnership, CreateNetworkPayload, InvitationDataV1 } from '@quiet/types' +import { FactoryGirl } from 'factory-girl' +import { expectSaga } from 'redux-saga-test-plan' +import { Socket } from '../../../types' +import { getFactory } from '../../../utils/tests/factories' +import { prepareStore } from '../../../utils/tests/prepareStore' +import { Store } from '../../store.types' +import { communitiesActions } from '../communities.slice' +import { joinNetworkSaga } from './joinNetwork.saga' + +describe('Join network saga', () => { + let store: Store + let factory: FactoryGirl + let validInvitationData: InvitationDataV1 + let validInvitationDeepUrl: string + const socket = { + emit: jest.fn(), + emitWithAck: jest.fn(() => { + return {} + }), + on: jest.fn(), + } as unknown as Socket + + beforeEach(async () => { + store = prepareStore().store + factory = await getFactory(store) + + validInvitationData = getValidInvitationUrlTestData(validInvitationDatav1[0]).data + validInvitationDeepUrl = getValidInvitationUrlTestData(validInvitationDatav1[0]).deepUrl() + }) + + it('creates network for v1 invitation data', async () => { + const payload: CreateNetworkPayload = { + ownership: CommunityOwnership.User, + peers: validInvitationData.pairs, + psk: validInvitationData.psk, + ownerOrbitDbIdentity: validInvitationData.ownerOrbitDbIdentity, + } + await expectSaga(joinNetworkSaga, socket, communitiesActions.joinNetwork(validInvitationData)) + .withState(store.getState()) + .put(communitiesActions.createNetwork(payload)) + .run() + }) +}) diff --git a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts index 7d5ac34e7e..c5ba56193d 100644 --- a/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts +++ b/packages/state-manager/src/utils/functions/invitationCode/invitationCode.test.ts @@ -1,27 +1,48 @@ +import { InvitationDataV1, InvitationDataVersion } from '@quiet/types' import { getInvitationCodes } from './invitationCode' -import { OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, PSK_PARAM_KEY, QUIET_JOIN_PAGE } from '@quiet/common' +import { + CID_PARAM_KEY, + INVITER_ADDRESS_PARAM_KEY, + OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, + PSK_PARAM_KEY, + QUIET_JOIN_PAGE, + SERVER_ADDRESS_PARAM_KEY, + TOKEN_PARAM_KEY, + validInvitationDatav1, + validInvitationDatav2, +} from '@quiet/common' + +const getUrlParamsPart = (url: string) => url.split(QUIET_JOIN_PAGE + '?')[1] describe('Invitation code helper', () => { - const peerId1 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA' - const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' - const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' - const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const psk = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=' - const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity' - const encodedPsk = encodeURIComponent(psk) - const encodedOwnerOrbitDbIdentity = encodeURIComponent(ownerOrbitDbIdentity) + const address = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' + const peerId = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' + const data: InvitationDataV1 = { + ...validInvitationDatav1[0], + pairs: [...validInvitationDatav1[0].pairs, { peerId: peerId, onionAddress: address }], + } + const urlParams = [ + [data.pairs[0].peerId, data.pairs[0].onionAddress], + [data.pairs[1].peerId, data.pairs[1].onionAddress], + [PSK_PARAM_KEY, data.psk], + [OWNER_ORBIT_DB_IDENTITY_PARAM_KEY, data.ownerOrbitDbIdentity], + ] + + const datav2 = validInvitationDatav2[0] + const urlParamsv2 = [ + [CID_PARAM_KEY, datav2.cid], + [TOKEN_PARAM_KEY, datav2.token], + [SERVER_ADDRESS_PARAM_KEY, datav2.serverAddress], + [INVITER_ADDRESS_PARAM_KEY, datav2.inviterAddress], + ] it('retrieves invitation code if url is a proper share url', () => { - const result = getInvitationCodes( - `${QUIET_JOIN_PAGE}#${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(url.href.replace('?', '#')) expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, + version: InvitationDataVersion.v1, + ...data, }) }) @@ -30,38 +51,33 @@ describe('Invitation code helper', () => { }) it('throws error if code does not contain psk', () => { - expect(() => getInvitationCodes(`${peerId1}=${address1}&${peerId2}=${address2}`)).toThrow() + const url = new URL(QUIET_JOIN_PAGE) + url.searchParams.append(urlParams[0][0], urlParams[0][1]) + url.searchParams.append(urlParams[1][0], urlParams[1][1]) + expect(() => getInvitationCodes(getUrlParamsPart(url.href))).toThrow() }) it('throws error if psk has invalid format', () => { - expect(() => getInvitationCodes(`${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=12345`)).toThrow() + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + url.searchParams.set(PSK_PARAM_KEY, '12345') + expect(() => getInvitationCodes(getUrlParamsPart(url.href))).toThrow() }) it('retrieves invitation code if url is a proper code', () => { - const result = getInvitationCodes( - `${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) + const url = new URL(QUIET_JOIN_PAGE) + urlParams.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(getUrlParamsPart(url.href)) expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, + version: InvitationDataVersion.v1, + ...data, }) }) - it('retrieves invitation code if url is a proper code', () => { - const result = getInvitationCodes( - `${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${encodedPsk}&${OWNER_ORBIT_DB_IDENTITY_PARAM_KEY}=${encodedOwnerOrbitDbIdentity}` - ) - expect(result).toEqual({ - pairs: [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ], - psk, - ownerOrbitDbIdentity, - }) + it('retrieves invitation code if url is a proper v2 code', () => { + const url = new URL(QUIET_JOIN_PAGE) + urlParamsv2.forEach(([key, value]) => url.searchParams.append(key, value)) + const result = getInvitationCodes(getUrlParamsPart(url.href)) + expect(result).toEqual(datav2) }) }) From 6e82f711b5ecd98f55b1762ef3f9ad86bda0b412 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 19 Mar 2024 15:33:34 +0100 Subject: [PATCH 15/21] feat: add one script for preparing AppImage for e2e tests --- package.json | 2 ++ packages/e2e-tests/.gitignore | 3 ++- packages/e2e-tests/package-lock.json | 19 +++++++++++++++++++ packages/e2e-tests/package.json | 6 ++++-- packages/e2e-tests/scripts/copyAppImage.js | 14 ++++++++++++++ .../src/tests/backwardsCompatibility.test.ts | 4 ++-- packages/e2e-tests/src/utils.ts | 11 +++++++++-- 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 packages/e2e-tests/scripts/copyAppImage.js diff --git a/package.json b/package.json index 5c2e28ce5c..c9e44e9828 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "start:desktop": "lerna run --scope @quiet/desktop start", "lint:all": "lerna run lint", "distAndRunE2ETests:mac:local": "lerna run --scope @quiet/desktop distMac:local && lerna run --scope e2e-tests test:localBinary --", + "e2e:linux:build": "lerna run --scope @quiet/backend webpack:prod && lerna run --scope @quiet/desktop distUbuntu && lerna run --scope e2e-tests linux:copy", + "e2e:linux:run": "lerna run --scope e2e-tests test --", "prepare": "husky", "lint-staged": "lerna run lint-staged" }, diff --git a/packages/e2e-tests/.gitignore b/packages/e2e-tests/.gitignore index 3d928d694b..6ca1a6bae7 100644 --- a/packages/e2e-tests/.gitignore +++ b/packages/e2e-tests/.gitignore @@ -2,4 +2,5 @@ lib/ screenshots/ .DS_Store -Quiet/Quiet** \ No newline at end of file +Quiet/Quiet** +.env \ No newline at end of file diff --git a/packages/e2e-tests/package-lock.json b/packages/e2e-tests/package-lock.json index bbcfdb5f02..7e083ff08a 100644 --- a/packages/e2e-tests/package-lock.json +++ b/packages/e2e-tests/package-lock.json @@ -21,6 +21,7 @@ "@types/jest": "^29.2.6", "@types/selenium-webdriver": "^4.1.10", "babel-jest": "^29.3.1", + "dotenv": "16.4.5", "lint-staged": "^15.2.2", "ts-jest": "^29.0.5", "typescript": "^4.9.3" @@ -2178,6 +2179,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-chromedriver": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-23.3.13.tgz", @@ -7821,6 +7834,12 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.2.tgz", "integrity": "sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==" }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true + }, "electron-chromedriver": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-23.3.13.tgz", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 9ea42b05f4..3628d6e810 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -16,7 +16,8 @@ "test": "cross-env TEST_MODE=true jest --runInBand --detectOpenHandles --forceExit", "test:localBinary": "cross-env TEST_MODE=true IS_LOCAL=true jest --runInBand --detectOpenHandles --forceExit --verbose --", "test:prod": "jest --runInBand --detectOpenHandles --forceExit", - "test:watch": "jest --watchAll" + "test:watch": "jest --watchAll", + "linux:copy": "node scripts/copyAppImage.js" }, "devDependencies": { "@quiet/eslint-config": "^2.0.2-alpha.0", @@ -25,7 +26,8 @@ "babel-jest": "^29.3.1", "lint-staged": "^15.2.2", "ts-jest": "^29.0.5", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "dotenv": "16.4.5" }, "dependencies": { "@quiet/common": "^2.0.2-alpha.1", diff --git a/packages/e2e-tests/scripts/copyAppImage.js b/packages/e2e-tests/scripts/copyAppImage.js new file mode 100644 index 0000000000..e5005007e2 --- /dev/null +++ b/packages/e2e-tests/scripts/copyAppImage.js @@ -0,0 +1,14 @@ +// Copy built AppImage to Quiet directory and set the version in .env file +const { execSync } = require('child_process') +const path = require('path') + +const desktop = path.join(__dirname, '..', '..', 'desktop') +const e2e = path.join(__dirname, '..') +const appVersion = JSON.parse(require('fs').readFileSync(path.join(desktop, 'package.json'), 'utf8')).version +const fileName = `Quiet-${appVersion}.AppImage` + +execSync(`rm -rf ${path.join(desktop, 'dist', 'squashfs-root')}`) + +console.log(`Copying file ${fileName} for e2e tests`) +execSync(`cp ${path.join(desktop, 'dist', fileName)} ${path.join(e2e, 'Quiet', fileName)}`) +execSync(`echo "FILE_NAME=${fileName}" > ${path.join(e2e, '.env')}`) diff --git a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts index 4d6f44b452..3f0bbd5f0a 100644 --- a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts +++ b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts @@ -8,7 +8,7 @@ import { RegisterUsernameModal, Sidebar, } from '../selectors' -import { BACKWARD_COMPATIBILITY_BASE_VERSION, copyInstallerFile, downloadInstaller } from '../utils' +import { BACKWARD_COMPATIBILITY_BASE_VERSION, BuildSetup, copyInstallerFile, downloadInstaller } from '../utils' jest.setTimeout(1200000) describe('Backwards Compatibility', () => { @@ -26,7 +26,7 @@ describe('Backwards Compatibility', () => { const loopMessages = 'ąbc'.split('') const newChannelName = 'mid-night-club' - const isAlpha = process.env.FILE_NAME?.toString().includes('alpha') + const isAlpha = BuildSetup.getEnvFileName()?.toString().includes('alpha') beforeAll(async () => { // download the old version of the app diff --git a/packages/e2e-tests/src/utils.ts b/packages/e2e-tests/src/utils.ts index c6cb437999..65282da7f3 100644 --- a/packages/e2e-tests/src/utils.ts +++ b/packages/e2e-tests/src/utils.ts @@ -5,6 +5,7 @@ import getPort from 'get-port' import path from 'path' import fs from 'fs' import { DESKTOP_DATA_DIR } from '@quiet/common' +import { config } from 'dotenv' export const BACKWARD_COMPATIBILITY_BASE_VERSION = '2.0.1' // Pre-latest production version const appImagesPath = `${__dirname}/../Quiet` @@ -43,11 +44,17 @@ export class BuildSetup { this.debugPort = await getPort() } + static getEnvFileName() { + const { parsed, error } = config() + console.log('Dotenv config', { parsed, error }) + return process.env.FILE_NAME + } + private getBinaryLocation() { console.log('filename', this.fileName) switch (process.platform) { case 'linux': - return `${__dirname}/../Quiet/${this.fileName ? this.fileName : process.env.FILE_NAME}` + return `${__dirname}/../Quiet/${this.fileName ? this.fileName : BuildSetup.getEnvFileName()}` case 'win32': return `${process.env.LOCALAPPDATA}\\Programs\\@quietdesktop\\Quiet.exe` case 'darwin': @@ -69,7 +76,7 @@ export class BuildSetup { } public getVersionFromEnv() { - const envFileName = process.env.FILE_NAME + const envFileName = this.getEnvFileName() if (!envFileName) { throw new Error('file name not specified') } From 5f80ac1b68ed76d1ea81fa88eb9750e167dc2315 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 19 Mar 2024 16:30:12 +0100 Subject: [PATCH 16/21] fix: 'copy app image for e2e' script --- packages/e2e-tests/scripts/copyAppImage.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/e2e-tests/scripts/copyAppImage.js b/packages/e2e-tests/scripts/copyAppImage.js index e5005007e2..8843b85535 100644 --- a/packages/e2e-tests/scripts/copyAppImage.js +++ b/packages/e2e-tests/scripts/copyAppImage.js @@ -1,14 +1,16 @@ // Copy built AppImage to Quiet directory and set the version in .env file -const { execSync } = require('child_process') +const { execFileSync } = require('child_process') const path = require('path') +const fs = require('fs') const desktop = path.join(__dirname, '..', '..', 'desktop') const e2e = path.join(__dirname, '..') const appVersion = JSON.parse(require('fs').readFileSync(path.join(desktop, 'package.json'), 'utf8')).version const fileName = `Quiet-${appVersion}.AppImage` -execSync(`rm -rf ${path.join(desktop, 'dist', 'squashfs-root')}`) +execFileSync('rm', ['-rf', path.join(desktop, 'dist', 'squashfs-root')]) console.log(`Copying file ${fileName} for e2e tests`) -execSync(`cp ${path.join(desktop, 'dist', fileName)} ${path.join(e2e, 'Quiet', fileName)}`) -execSync(`echo "FILE_NAME=${fileName}" > ${path.join(e2e, '.env')}`) +execFileSync('cp', [path.join(desktop, 'dist', fileName), path.join(e2e, 'Quiet', fileName)]) + +fs.writeFileSync(path.join(e2e, '.env'), `FILE_NAME=${fileName}`) From 7471a245cfa135fc210ab2587edbefcb7a9aed3a Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 19 Mar 2024 16:37:38 +0100 Subject: [PATCH 17/21] fix: getting env file name in e2e tests --- packages/e2e-tests/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/src/utils.ts b/packages/e2e-tests/src/utils.ts index 65282da7f3..bcba4e5350 100644 --- a/packages/e2e-tests/src/utils.ts +++ b/packages/e2e-tests/src/utils.ts @@ -76,7 +76,7 @@ export class BuildSetup { } public getVersionFromEnv() { - const envFileName = this.getEnvFileName() + const envFileName = BuildSetup.getEnvFileName() if (!envFileName) { throw new Error('file name not specified') } From 967665d3c6042a9c7479b71076f3ba1199a26a96 Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 20 Mar 2024 16:10:49 +0100 Subject: [PATCH 18/21] chore: remove unused code responsible for locking invitation link form on deep linking --- packages/backend/jestSetup.js | 13 +++++++----- .../JoinCommunity/JoinCommunity.tsx | 6 ------ .../PerformCommunityActionComponent.tsx | 20 ------------------- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/packages/backend/jestSetup.js b/packages/backend/jestSetup.js index 269624f2bd..b82077ab46 100644 --- a/packages/backend/jestSetup.js +++ b/packages/backend/jestSetup.js @@ -1,11 +1,14 @@ -import { setEngine, CryptoEngine } from'pkijs' +import { setEngine, CryptoEngine } from 'pkijs' import { Crypto } from '@peculiar/webcrypto' -const crypto = new Crypto(); -global.crypto = crypto; +const crypto = new Crypto() +global.crypto = crypto -setEngine('newEngine', new CryptoEngine({ +setEngine( + 'newEngine', + new CryptoEngine({ name: 'newEngine', // @ts-ignore crypto: crypto, - })) + }) +) diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index f8d5b899b8..0d03c60f01 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -15,10 +15,6 @@ const JoinCommunity = () => { const currentCommunity = useSelector(communities.selectors.currentCommunity) const currentIdentity = useSelector(identity.selectors.currentIdentity) - // Invitation link data should be already available if user joined via deep link - const invitationCodes = useSelector(communities.selectors.invitationCodes) - const psk = useSelector(communities.selectors.psk) - const joinCommunityModal = useModal(ModalName.joinCommunityModal) const createCommunityModal = useModal(ModalName.createCommunityModal) @@ -66,8 +62,6 @@ const JoinCommunity = () => { hasReceivedResponse={Boolean(currentIdentity && !currentIdentity.userCertificate)} revealInputValue={revealInputValue} handleClickInputReveal={handleClickInputReveal} - invitationCode={invitationCodes} - psk={psk} /> ) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index 6d815bf958..5bff2b131f 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -137,9 +137,6 @@ export interface PerformCommunityActionProps { hasReceivedResponse: boolean revealInputValue?: boolean handleClickInputReveal?: () => void - invitationCode?: InvitationPair[] - psk?: string - ownerOrbitDbIdentity?: string } export const PerformCommunityActionComponent: React.FC = ({ @@ -153,9 +150,6 @@ export const PerformCommunityActionComponent: React.FC { const [formSent, setFormSent] = useState(false) @@ -219,20 +213,6 @@ export const PerformCommunityActionComponent: React.FC { - if (communityOwnership === CommunityOwnership.User && invitationCode?.length && psk && ownerOrbitDbIdentity) { - setFormSent(true) - setValue('name', composeInvitationShareUrl({ pairs: invitationCode, psk, ownerOrbitDbIdentity })) - } - }, [communityOwnership, invitationCode]) - useEffect(() => { if (!open) { setValue('name', '') From 9f84ffeb223ebe1c34b57bbaea79fe3b9b3234fe Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 20 Mar 2024 16:17:48 +0100 Subject: [PATCH 19/21] chore: update changelog --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30481dbdd9..b3b4463fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ [unreleased] -* Refactored package.json to have consistent license "GPL-3.0-or-later" - +# Features: + +* Add support for new format of invitation link: `c=&t=&s=&i=` + # Refactorings: * Use ack for CREATE_NETWORK and simplify @@ -10,6 +12,10 @@ * Allow JPEG and GIF files as profile photos ([#2332](https://github.com/TryQuiet/quiet/issues/2332)) +# Other: + +* Refactored package.json to have consistent license "GPL-3.0-or-later" + [2.1.2] # Refactorings: From a529c7fab681e889299048b1740231d2f94443b3 Mon Sep 17 00:00:00 2001 From: Emi Date: Tue, 9 Apr 2024 14:44:13 +0200 Subject: [PATCH 20/21] fix: mobile tests --- .../desktop/src/renderer/testUtils/index.ts | 1 - .../src/rtl-tests/channel.add.test.tsx | 2 +- .../src/rtl-tests/channel.main.test.tsx | 2 +- .../src/rtl-tests/community.create.test.tsx | 2 +- .../src/rtl-tests/community.join.test.tsx | 2 +- .../store/init/deepLink/deepLink.saga.test.ts | 4 +-- .../mobile/src/tests/deep.linking.test.tsx | 28 +++++++++++++++---- .../createNetwork/createNetwork.saga.ts | 8 ++++-- packages/types/src/index.ts | 1 + .../testUtils/socket.ts => types/src/test.ts} | 0 10 files changed, 35 insertions(+), 15 deletions(-) rename packages/{desktop/src/renderer/testUtils/socket.ts => types/src/test.ts} (100%) diff --git a/packages/desktop/src/renderer/testUtils/index.ts b/packages/desktop/src/renderer/testUtils/index.ts index 2a8f54c14e..8138beb934 100644 --- a/packages/desktop/src/renderer/testUtils/index.ts +++ b/packages/desktop/src/renderer/testUtils/index.ts @@ -1,4 +1,3 @@ export * from './generateMessages' export * from './prepareStore' export * from './renderComponent' -export * from './socket' diff --git a/packages/desktop/src/rtl-tests/channel.add.test.tsx b/packages/desktop/src/rtl-tests/channel.add.test.tsx index 06534239b6..027949c65c 100644 --- a/packages/desktop/src/rtl-tests/channel.add.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.add.test.tsx @@ -6,7 +6,7 @@ import { act } from 'react-dom/test-utils' import { take } from 'typed-redux-saga' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { renderComponent } from '../renderer/testUtils/renderComponent' import { prepareStore } from '../renderer/testUtils/prepareStore' import { StoreKeys } from '../renderer/store/store.keys' diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index 924111e61b..c4148b6ac0 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -6,7 +6,7 @@ import { apply, take } from 'typed-redux-saga' import userEvent from '@testing-library/user-event' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { renderComponent } from '../renderer/testUtils/renderComponent' import { prepareStore } from '../renderer/testUtils/prepareStore' import Channel from '../renderer/components/Channel/Channel' diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 44653b7730..a90f5660fb 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -13,7 +13,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import { CreateCommunityDictionary } from '../renderer/components/CreateJoinCommunity/community.dictionary' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { Community, type InitCommunityPayload, diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index a8c469bae0..cb6f613a99 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -13,7 +13,7 @@ import { ModalName } from '../renderer/sagas/modals/modals.types' import { JoinCommunityDictionary } from '../renderer/components/CreateJoinCommunity/community.dictionary' import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' -import { socketEventData } from '../renderer/testUtils/socket' +import { socketEventData } from '@quiet/types' import { communities, RegisterUserCertificatePayload, diff --git a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts index a954dcff94..a74e6f8eb2 100644 --- a/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts +++ b/packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts @@ -125,12 +125,12 @@ describe('deepLinkSaga', () => { }) ) + community.psk = validData.psk + store.dispatch(communities.actions.addNewCommunity(community)) store.dispatch(communities.actions.setCurrentCommunity(community.id)) - store.dispatch(communities.actions.savePSK(validData.psk)) - const reducer = combineReducers(reducers) await expectSaga(deepLinkSaga, initActions.deepLink(validCode)) .withReducer(reducer) diff --git a/packages/mobile/src/tests/deep.linking.test.tsx b/packages/mobile/src/tests/deep.linking.test.tsx index d3aa036fdf..651ec706d2 100644 --- a/packages/mobile/src/tests/deep.linking.test.tsx +++ b/packages/mobile/src/tests/deep.linking.test.tsx @@ -9,16 +9,32 @@ import { prepareStore } from './utils/prepareStore' import { renderComponent } from './utils/renderComponent' import { initActions } from '../store/init/init.slice' import { validInvitationCodeTestData, getValidInvitationUrlTestData } from '@quiet/common' -import { communities } from '@quiet/state-manager' +import { communities, createPeerIdTestHelper } from '@quiet/state-manager' +import { NetworkInfo, SocketActionTypes, socketEventData } from '@quiet/types' + +const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const action = input[0] + if (action === SocketActionTypes.CREATE_NETWORK) { + const data: NetworkInfo = { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privateKey', + }, + peerId: createPeerIdTestHelper(), + } + return data + } +} describe('Deep linking', () => { let socket: MockedSocket beforeEach(async () => { socket = new MockedSocket() - // @ts-ignore - socket.emitWithAck = jest.fn() ioMock.mockImplementation(() => socket) + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl }) test('does not override network data if triggered twice', async () => { @@ -55,13 +71,13 @@ describe('Deep linking', () => { "Communities/joinNetwork", "Communities/createNetwork", "Navigation/replaceScreen", - "Communities/setInvitationCodes", - "Communities/savePSK", "Communities/addNewCommunity", - "Navigation/replaceScreen", "Communities/setCurrentCommunity", "Communities/setInvitationCodes", + "Identity/addNewIdentity", "Init/deepLink", + "Init/resetDeepLink", + "Navigation/replaceScreen", ] `) diff --git a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts index ed84a06819..1247bcfb16 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -5,7 +5,7 @@ import { generateId } from '../../../utils/cryptography/cryptography' import { communitiesActions } from '../communities.slice' import { identityActions } from '../../identity/identity.slice' import { createRootCA } from '@quiet/identity' -import { type Community, CommunityOwnership, type Identity, SocketActionTypes } from '@quiet/types' +import { type Community, CommunityOwnership, type Identity, SocketActionTypes, NetworkInfo } from '@quiet/types' import { generateDmKeyPair } from '../../../utils/cryptography/cryptography' import { Socket, applyEmitParams } from '../../../types' @@ -18,7 +18,11 @@ export function* createNetworkSaga( // Community IDs are only local identifiers const id = yield* call(generateId) - const network = yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.CREATE_NETWORK, id)) + const network: NetworkInfo = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.CREATE_NETWORK, id) + ) // TODO: Move CA generation to backend when creating Community let CA: null | { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3d16fdbe16..0a7902637e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -9,3 +9,4 @@ export * from './message' export * from './files' export * from './channel' export * from './network' +export * from './test' diff --git a/packages/desktop/src/renderer/testUtils/socket.ts b/packages/types/src/test.ts similarity index 100% rename from packages/desktop/src/renderer/testUtils/socket.ts rename to packages/types/src/test.ts From fcd8b2b53d26092209fbd68a2ce87ae2dbb497f9 Mon Sep 17 00:00:00 2001 From: Emi Date: Wed, 10 Apr 2024 13:57:43 +0200 Subject: [PATCH 21/21] fix: long failing backend test --- .../connections-manager.service.tor.spec.ts | 19 +++++++++++++++---- .../nest/libp2p/process-in-chunks.service.ts | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts index fb41bf4aa2..8dd64ef253 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts @@ -5,8 +5,7 @@ import { CustomEvent } from '@libp2p/interfaces/events' import { jest, beforeEach, describe, it, expect, afterEach } from '@jest/globals' import { communities, getFactory, identity, prepareStore, Store } from '@quiet/state-manager' import { createPeerId, createTmpDir, libp2pInstanceParams, removeFilesFromDir, tmpQuietDirPath } from '../common/utils' - -import { NetworkStats, type Community, type Identity, type InitCommunityPayload } from '@quiet/types' +import { NetworkStats, type Community, type Identity } from '@quiet/types' import { LazyModuleLoader } from '@nestjs/core' import { TestingModule, Test } from '@nestjs/testing' import { FactoryGirl } from 'factory-girl' @@ -113,7 +112,6 @@ beforeEach(async () => { }) afterEach(async () => { - await libp2pService?.libp2pInstance?.stop() if (connectionsManagerService) { await connectionsManagerService.closeAllServices() } @@ -122,6 +120,10 @@ afterEach(async () => { describe('Connections manager', () => { it('saves peer stats when peer has been disconnected', async () => { + // @ts-expect-error + libp2pService.processInChunksService.init = jest.fn() + // @ts-expect-error + libp2pService.processInChunksService.process = jest.fn() class RemotePeerEventDetail { peerId: string @@ -137,6 +139,10 @@ describe('Connections manager', () => { // Peer connected await connectionsManagerService.init() + await connectionsManagerService.launchCommunity({ + community, + network: { peerId: userIdentity.peerId, hiddenService: userIdentity.hiddenService }, + }) libp2pService.connectedPeers.set(peerId.toString(), DateTime.utc().valueOf()) // Peer disconnected @@ -145,11 +151,16 @@ describe('Connections manager', () => { remotePeer: new RemotePeerEventDetail(peerId.toString()), remoteAddr: new RemotePeerEventDetail(remoteAddr), } + await waitForExpect(async () => { + expect(libp2pService.libp2pInstance).not.toBeUndefined() + }, 2_000) libp2pService.libp2pInstance?.dispatchEvent( new CustomEvent('peer:disconnect', { detail: peerDisconectEventDetail }) ) + await waitForExpect(async () => { + expect(libp2pService.connectedPeers.size).toEqual(0) + }, 2000) - expect(libp2pService.connectedPeers.size).toEqual(0) await waitForExpect(async () => { expect(await localDbService.get(LocalDBKeys.PEERS)).not.toBeNull() }, 2000) diff --git a/packages/backend/src/nest/libp2p/process-in-chunks.service.ts b/packages/backend/src/nest/libp2p/process-in-chunks.service.ts index 2e7b30588b..b87892ea3d 100644 --- a/packages/backend/src/nest/libp2p/process-in-chunks.service.ts +++ b/packages/backend/src/nest/libp2p/process-in-chunks.service.ts @@ -13,7 +13,7 @@ export class ProcessInChunksService extends EventEmitter { super() } - public init(data: T[], processItem: (arg: T) => Promise, chunkSize: number = DEFAULT_CHUNK_SIZE) { + public init(data: T[] = [], processItem: (arg: T) => Promise, chunkSize: number = DEFAULT_CHUNK_SIZE) { this.data = data this.processItem = processItem this.chunkSize = chunkSize