Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
191 lines
7.1 KiB
TypeScript
191 lines
7.1 KiB
TypeScript
import type { IRoomNativeFederated, IUser } from '@rocket.chat/core-typings';
|
|
import { Visibility } from 'matrix-js-sdk';
|
|
|
|
import { api } from '../../../../../apps/meteor/tests/data/api-data';
|
|
import { acceptRoomInvite } from '../../../../../apps/meteor/tests/data/rooms.helper';
|
|
import {
|
|
type IRequestConfig,
|
|
createUser,
|
|
deleteUser,
|
|
getRequestConfig,
|
|
getUserByUsername,
|
|
} from '../../../../../apps/meteor/tests/data/users.helper';
|
|
import { IS_EE } from '../../../../../apps/meteor/tests/e2e/config/constants';
|
|
import { retry } from '../../../../../apps/meteor/tests/end-to-end/api/helpers/retry';
|
|
import { federationConfig } from '../helper/config';
|
|
import { SynapseClient } from '../helper/synapse-client';
|
|
|
|
const remoteUser = federationConfig.hs1.firstContactUser;
|
|
const localUser = federationConfig.rc1.firstContactUser;
|
|
|
|
/**
|
|
* Every other federation spec reuses Synapse users that earlier specs have already pulled into
|
|
* the local database, so the membership handlers always find a complete user document and the
|
|
* creation path is never observed. This spec reserves a Synapse user nobody else touches, removes
|
|
* the local document if a previous run left one behind, and only then lets that user reach in.
|
|
*
|
|
* The user is therefore created by the inbound invite itself, which is the one code path whose
|
|
* result is consumed rather than discarded.
|
|
*/
|
|
(IS_EE ? describe : describe.skip)('Federation first contact', () => {
|
|
let rc1AdminRequestConfig: IRequestConfig;
|
|
let rc1InviteeRequestConfig: IRequestConfig;
|
|
let hs1FirstContactApp: SynapseClient;
|
|
|
|
const forgetRemoteUserLocally = async () => {
|
|
const known = await getUserByUsername(remoteUser.matrixUserId, rc1AdminRequestConfig);
|
|
if (known?._id) {
|
|
await deleteUser({ _id: known._id }, { confirmRelinquish: true }, rc1AdminRequestConfig);
|
|
}
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
rc1AdminRequestConfig = await getRequestConfig(
|
|
federationConfig.rc1.url,
|
|
federationConfig.rc1.adminUser,
|
|
federationConfig.rc1.adminPassword,
|
|
);
|
|
|
|
const existingLocalUser = await getUserByUsername(localUser.username, rc1AdminRequestConfig);
|
|
if (!existingLocalUser?._id) {
|
|
await createUser(
|
|
{
|
|
username: localUser.username,
|
|
password: localUser.password,
|
|
email: `${localUser.username}@rocket.chat`,
|
|
name: localUser.username,
|
|
},
|
|
rc1AdminRequestConfig,
|
|
);
|
|
}
|
|
|
|
rc1InviteeRequestConfig = await getRequestConfig(federationConfig.rc1.url, localUser.username, localUser.password);
|
|
|
|
// a previous run leaves the user behind, which would make this spec exercise the warm path instead
|
|
await forgetRemoteUserLocally();
|
|
|
|
hs1FirstContactApp = new SynapseClient(federationConfig.hs1.url, remoteUser.username, remoteUser.password);
|
|
await hs1FirstContactApp.initialize();
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
// leave the environment cold so a rerun against persistent containers still tests first contact
|
|
await forgetRemoteUserLocally();
|
|
await hs1FirstContactApp?.close();
|
|
});
|
|
|
|
it('should not know the remote user before the first interaction', async () => {
|
|
const response = await rc1AdminRequestConfig.request
|
|
.get(api('users.info'))
|
|
.set(rc1AdminRequestConfig.credentials)
|
|
.query({ username: remoteUser.matrixUserId });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body).toHaveProperty('success', false);
|
|
});
|
|
|
|
describe('when the first interaction is an invite from Synapse', () => {
|
|
let channelName: string;
|
|
let synapseRoomId: string;
|
|
let federatedRoomId: string;
|
|
|
|
beforeAll(async () => {
|
|
channelName = `fed-first-contact-${Date.now()}`;
|
|
synapseRoomId = await hs1FirstContactApp.createRoom(channelName, Visibility.Private);
|
|
|
|
await hs1FirstContactApp.inviteUserToRoom(synapseRoomId, localUser.matrixUserId);
|
|
}, 60000);
|
|
|
|
it('should create the room on the Rocket.Chat side', async () => {
|
|
await retry(
|
|
'waiting for the federated room to reach RC',
|
|
async () => {
|
|
// rooms.get is scoped to the caller's subscriptions, and only the invitee is in this room
|
|
const response = await rc1InviteeRequestConfig.request.get(api('rooms.get')).set(rc1InviteeRequestConfig.credentials).expect(200);
|
|
|
|
const rcRoom = response.body.update.find(
|
|
(room: IRoomNativeFederated) => room.federation?.mrid === synapseRoomId,
|
|
) as IRoomNativeFederated | null;
|
|
|
|
expect(rcRoom).toBeTruthy();
|
|
federatedRoomId = rcRoom!._id;
|
|
},
|
|
{ retries: 10, delayMs: 2000 },
|
|
);
|
|
}, 30000);
|
|
|
|
// this covers the persisted document only. The shape of the object the invite handler reads back
|
|
// is not observable from here — it belongs to the createOrUpdateFederatedUser and
|
|
// getOrCreateFederatedUser unit tests, and when it regresses the room and subscription
|
|
// assertions below are what fail.
|
|
it('should persist the remote user locally as a complete document', async () => {
|
|
const response = await rc1AdminRequestConfig.request
|
|
.get(api('users.info'))
|
|
.set(rc1AdminRequestConfig.credentials)
|
|
.query({ username: remoteUser.matrixUserId })
|
|
.expect(200);
|
|
|
|
expect(response.body.user).toMatchObject({
|
|
username: remoteUser.matrixUserId,
|
|
name: remoteUser.matrixUserId,
|
|
type: 'user',
|
|
active: true,
|
|
federated: true,
|
|
roles: expect.arrayContaining(['federated-external']),
|
|
});
|
|
expect(response.body.user.createdAt).toBeTruthy();
|
|
});
|
|
|
|
it('should create the invited subscription for the local user', async () => {
|
|
const response = await rc1InviteeRequestConfig.request
|
|
.get(api('subscriptions.getOne'))
|
|
.set(rc1InviteeRequestConfig.credentials)
|
|
.query({ roomId: federatedRoomId })
|
|
.expect(200);
|
|
|
|
expect(response.body.subscription).toBeTruthy();
|
|
expect(response.body.subscription).toHaveProperty('status', 'INVITED');
|
|
});
|
|
|
|
it('should let the local user accept the invite', async () => {
|
|
const response = await acceptRoomInvite(federatedRoomId, rc1InviteeRequestConfig);
|
|
|
|
expect(response).toHaveProperty('success', true);
|
|
});
|
|
|
|
it('should reach the local user on the Synapse side', async () => {
|
|
await retry(
|
|
'waiting for the local user to appear as joined on Synapse',
|
|
async () => {
|
|
const member = await hs1FirstContactApp.findRoomMember(channelName, localUser.matrixUserId);
|
|
|
|
expect(member).not.toBeNull();
|
|
expect(member!.membership).toBe('join');
|
|
},
|
|
{ retries: 10, delayMs: 2000 },
|
|
);
|
|
}, 30000);
|
|
|
|
// a named, invite-only Synapse room becomes a private group on RC, so groups.members applies.
|
|
// it gates on canAccessRoomAsync without includeInvitations, meaning the accepted subscription
|
|
// has to have settled first — hence the retry and the ordering after the Synapse round-trip
|
|
it('should list the remote user among the room members', async () => {
|
|
await retry(
|
|
'waiting for the room members to include the remote user',
|
|
async () => {
|
|
const response = await rc1InviteeRequestConfig.request
|
|
.get(api('groups.members'))
|
|
.set(rc1InviteeRequestConfig.credentials)
|
|
.query({ roomId: federatedRoomId })
|
|
.expect(200);
|
|
|
|
expect(response.body.members).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ username: remoteUser.matrixUserId } as Partial<IUser>)]),
|
|
);
|
|
},
|
|
{ retries: 5, delayMs: 2000 },
|
|
);
|
|
}, 30000);
|
|
});
|
|
});
|