Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b0c8994
use getRoomByTypeAndName to get more info
Rohit3523 Apr 7, 2026
76c5ec9
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Apr 23, 2026
32ea305
improve
Rohit3523 Apr 23, 2026
1af2c27
chore: format code and fix lint issues
Rohit3523 Apr 23, 2026
252c6b2
Added E2E test
Rohit3523 Apr 23, 2026
2f3c851
Merge branch 'private-channel-deeplink-fail' of https://github.com/Ro…
Rohit3523 Apr 23, 2026
4607327
chore: format code and fix lint issues
Rohit3523 Apr 23, 2026
180d1b8
Move group link to assorted
Rohit3523 Apr 25, 2026
97fc070
Merge branch 'private-channel-deeplink-fail' of https://github.com/Ro…
Rohit3523 Apr 25, 2026
9d42eca
update
Rohit3523 Apr 25, 2026
9e73065
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Apr 27, 2026
e2caede
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Jul 29, 2026
ceb8a17
Added typing for groups.open
Rohit3523 Jul 29, 2026
6fa65c4
remove ts-ignore
Rohit3523 Jul 29, 2026
b62c0dc
added test
Rohit3523 Jul 29, 2026
8e8e55f
chore: format code and fix lint issues
Rohit3523 Jul 29, 2026
198b39c
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Aug 3, 2026
f059b8f
using getinfo for channel too
Rohit3523 Aug 4, 2026
2d1a8f0
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Aug 6, 2026
01c8598
test update
Rohit3523 Aug 6, 2026
f27c93c
rerun ci
Rohit3523 Aug 6, 2026
3ef413e
revert
Rohit3523 Aug 6, 2026
a992162
Merge branch 'develop' into private-channel-deeplink-fail
OtavioStasiak Aug 12, 2026
86e45ac
Merge branch 'develop' into private-channel-deeplink-fail
OtavioStasiak Aug 12, 2026
1a2b919
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Aug 13, 2026
7140ddb
added deeplink test in sniffler
Rohit3523 Aug 13, 2026
5da8ebf
create channel and group using test account
Rohit3523 Aug 13, 2026
af98bc4
test fix
Rohit3523 Aug 13, 2026
387ec21
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Aug 19, 2026
bcfc461
chore: format code and fix lint issues
Rohit3523 Aug 19, 2026
84ac63a
improve code
Rohit3523 Aug 19, 2026
1441239
Merge branch 'develop' into private-channel-deeplink-fail
Rohit3523 Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/__tests__/fixtures/scenario-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@
},
{
"id": "C3",
"name": "shared saga fans wide: sagas/rooms.js -> eleven flows",
"name": "shared saga fans wide: sagas/rooms.js -> twelve flows",
"category": "real-domain",
"input": { "diff": ["app/sagas/rooms.js"] },
"expectedShards": [1, 5, 6, 7, 8, 11, 13, 14],
"expectedShards": [1, 5, 6, 7, 8, 11, 12, 13, 14],
"expectedShouldRun": true,
"assertableIn": ["map", "ci"]
},
Expand Down
63 changes: 63 additions & 0 deletions .maestro/tests/assorted/channel-deeplink.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
appId: ${APP_ID}
name: Channel Deeplink
onFlowStart:
- runFlow: '../../helpers/setup.yaml'
onFlowComplete:
- evalScript: ${output.utils.deleteCreatedUsers()}
tags:
- test-12

---
- evalScript: ${output.user = output.utils.createUser()}
- evalScript: ${output.group = output.utils.createRandomRoom(output.user.username, output.user.password, 'p')}
- evalScript: ${output.channel = output.utils.createRandomRoom(output.user.username, output.user.password, 'c')}

- runFlow:
file: '../../helpers/login-with-deeplink.yaml'
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}

# open group using name
- killApp
- runFlow:
Comment thread
Rohit3523 marked this conversation as resolved.
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.group.name)}
- extendedWaitUntil:
visible:
id: 'room-view-title-${output.group.name}'
timeout: 60000

# open group using id
- killApp
- runFlow:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=group/', output.group._id)}
- extendedWaitUntil:
visible:
id: 'room-view-title-${output.group.name}'
timeout: 60000

# open channel using name
- killApp
- runFlow:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=channel/', output.channel.name)}
- extendedWaitUntil:
visible:
id: 'room-view-title-${output.channel.name}'
timeout: 60000

# open channel using id
- killApp
- runFlow:
file: '../../helpers/open-deeplink.yaml'
env:
link: ${output.utils.getDeepLink('room', output.data.server, 'path=channel/', output.channel._id)}
- extendedWaitUntil:
visible:
id: 'room-view-title-${output.channel.name}'
timeout: 60000
11 changes: 11 additions & 0 deletions .sniffler/test-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
"app/sagas/createChannel.js"
]
},
{
"test": ".maestro/tests/assorted/channel-deeplink.yaml",
"dependsOn": [
"app/views/RoomView/**",
"app/views/RoomsListView/**",
"app/sagas/deepLinking.js",
"app/sagas/room.js",
"app/sagas/login.js",
"app/sagas/rooms.js"
]
},
{
"test": ".maestro/tests/assorted/change-avatar.yaml",
"dependsOn": ["app/views/ChangeAvatarView/**", "app/views/ProfileView/**"]
Expand Down
5 changes: 5 additions & 0 deletions app/definitions/rest/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,9 @@ export type GroupsEndpoints = {
messages: IMessageFromServer[];
};
};
'groups.open': {
POST: (params: { roomId: IServerRoom['_id'] }) => {
success: boolean;
};
};
};
152 changes: 152 additions & 0 deletions app/lib/methods/canOpenRoom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { canOpenRoom } from './canOpenRoom';
import sdk from '../services/sdk';
import { getRoomByTypeAndName } from '../services/restApi';

jest.mock('../services/sdk', () => ({
__esModule: true,
default: {
post: jest.fn()
}
}));

jest.mock('../services/restApi', () => ({
getRoomByTypeAndName: jest.fn()
}));

const mockedSdkPost = sdk.post as jest.Mock;
const mockedGetRoomByTypeAndName = getRoomByTypeAndName as jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
});

describe('canOpenRoom — GROUP deeplink', () => {
const roomFixture = { _id: 'group-id-1', name: 'test-group', t: 'p' };

it('returns room when getRoomByTypeAndName succeeds and groups.open succeeds', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);
mockedSdkPost.mockResolvedValue({ success: true });

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(mockedGetRoomByTypeAndName).toHaveBeenCalledWith('p', 'test-group');
expect(mockedSdkPost).toHaveBeenCalledWith('groups.open', { roomId: 'group-id-1' });
expect(result).toEqual({ ...roomFixture, rid: 'group-id-1' });
});

it('returns room when the path holds a room id instead of a room name', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);
mockedSdkPost.mockResolvedValue({ success: true });

const result = await canOpenRoom({ rid: '', path: 'group/6997e23f362b278aeb3d369b' });

expect(mockedGetRoomByTypeAndName).toHaveBeenCalledWith('p', '6997e23f362b278aeb3d369b');
expect(mockedSdkPost).toHaveBeenCalledWith('groups.open', { roomId: 'group-id-1' });
expect(result).toEqual({ ...roomFixture, rid: 'group-id-1' });
});

it('returns room when groups.open reports the group is already open', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);
mockedSdkPost.mockRejectedValue({ data: { error: 'The private group, test-group, is already open for the sender' } });

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(result).toEqual({ ...roomFixture, rid: 'group-id-1' });
});

it('opens the group by rid without resolving the path segment', async () => {
mockedSdkPost.mockResolvedValue({ success: true });

const result = await canOpenRoom({ rid: 'group-id-1', path: 'group/test-group' });

expect(mockedGetRoomByTypeAndName).not.toHaveBeenCalled();
expect(mockedSdkPost).toHaveBeenCalledWith('groups.open', { roomId: 'group-id-1' });
expect(result).toEqual({ rid: 'group-id-1' });
});

it('returns false when getRoomByTypeAndName returns null', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(null);

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(mockedSdkPost).not.toHaveBeenCalled();
expect(result).toBe(false);
});

it('returns false when getRoomByTypeAndName returns object without _id', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue({});

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(result).toBe(false);
});

it('returns false when getRoomByTypeAndName throws', async () => {
mockedGetRoomByTypeAndName.mockRejectedValue(new Error('not found'));

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(result).toBe(false);
});

it('returns false when groups.open throws a non-already-open error', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);
mockedSdkPost.mockRejectedValue({ data: { error: 'forbidden' } });

const result = await canOpenRoom({ rid: '', path: 'group/test-group' });

expect(result).toBe(false);
});
});

describe('canOpenRoom — CHANNEL deeplink', () => {
const roomFixture = { _id: 'channel-id-1', name: 'test-channel', t: 'c' };

it('returns room when the path holds a room name', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);

const result = await canOpenRoom({ rid: '', path: 'channel/test-channel' });

expect(mockedGetRoomByTypeAndName).toHaveBeenCalledWith('c', 'test-channel');
expect(mockedSdkPost).not.toHaveBeenCalled();
expect(result).toEqual({ ...roomFixture, rid: 'channel-id-1' });
});

it('returns room when the path holds a room id instead of a room name', async () => {
mockedGetRoomByTypeAndName.mockResolvedValue(roomFixture);

const result = await canOpenRoom({ rid: '', path: 'channel/6997e23f362b278aeb3d369b' });

expect(mockedGetRoomByTypeAndName).toHaveBeenCalledWith('c', '6997e23f362b278aeb3d369b');
expect(result).toEqual({ ...roomFixture, rid: 'channel-id-1' });
});

it('returns the rid without resolving the path segment when rid is provided', async () => {
const result = await canOpenRoom({ rid: 'channel-id-1', path: 'channel/test-channel' });

expect(mockedGetRoomByTypeAndName).not.toHaveBeenCalled();
expect(mockedSdkPost).not.toHaveBeenCalled();
expect(result).toEqual({ rid: 'channel-id-1' });
});

it('returns false when getRoomByTypeAndName throws', async () => {
mockedGetRoomByTypeAndName.mockRejectedValue(new Error('not found'));

const result = await canOpenRoom({ rid: '', path: 'channel/test-channel' });

expect(result).toBe(false);
});
});

describe('canOpenRoom — other paths', () => {
it('returns false when no path and no rid', async () => {
const result = await canOpenRoom({ rid: '', path: '' });
expect(result).toBe(false);
});

it('returns false when path does not match any type', async () => {
const result = await canOpenRoom({ rid: '', path: 'unknown/foo' });
expect(mockedGetRoomByTypeAndName).not.toHaveBeenCalled();
expect(result).toBe(false);
});
});
60 changes: 32 additions & 28 deletions app/lib/methods/canOpenRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@ import { ERoomTypes } from '../../definitions';
import database from '../database';
import sdk from '../services/sdk';
import { createDirectMessage } from './createDirectMessage';

const restTypes = {
channel: 'channels',
direct: 'im',
group: 'groups'
};
import { getRoomByTypeAndName } from '../services/restApi';

async function open({ type, rid, name }: { type: ERoomTypes; rid: string; name: string }) {
try {
const params = rid ? { roomId: rid } : { roomName: name };

// if it's a direct link without rid we'll create a new dm
// if the dm already exists it'll return the existent
if (type === ERoomTypes.DIRECT && !rid) {
Expand All @@ -26,29 +19,40 @@ async function open({ type, rid, name }: { type: ERoomTypes; rid: string; name:
}
}

// if it's a group we need to check if you can open
if (type === ERoomTypes.GROUP) {
try {
// RC 0.61.0
// @ts-ignore
await sdk.post(`${restTypes[type]}.open`, params);
} catch (e: any) {
if (!(e.data && /is already open/.test(e.data.error))) {
return false;
if (type === ERoomTypes.CHANNEL || type === ERoomTypes.GROUP) {
let roomId = rid;
let room = null;

// The path segment of a deep link may hold either a room name or a room id.
// getRoomByTypeAndName resolves both, unlike the REST endpoints, which match
Comment thread
Rohit3523 marked this conversation as resolved.
// roomName exactly and return "not found" when handed an id.
if (!roomId) {
const roomType = type === ERoomTypes.GROUP ? 'p' : 'c';
room = await getRoomByTypeAndName(roomType, name);
roomId = room?._id;
}

if (!roomId) {
return false;
}

// a group has to be open before it can be read
if (type === ERoomTypes.GROUP) {
try {
// RC 0.61.0
await sdk.post('groups.open', { roomId });
} catch (e: any) {
if (!(e.data && /is already open/.test(e.data.error))) {
return false;
}
}
}
}

// if it's a channel or group and the link don't have rid
// we'll get info from the room
if ((type === ERoomTypes.CHANNEL || type === ERoomTypes.GROUP) && !rid) {
// RC 0.72.0
// @ts-ignore
const result: any = await sdk.get(`${restTypes[type]}.info`, params);
if (result.success) {
const room = result[type];
room.rid = room._id;
return room;
if (room) {
return {
...room,
rid: roomId
};
}
}

Expand Down
Loading