test(federation): improve and slim down config and scripts (#37769)

This commit is contained in:
Diego Sampaio 2025-12-11 16:44:14 -03:00 committed by GitHub
parent 38a736f89c
commit 2527c47b0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 177 additions and 111 deletions

View File

@ -656,6 +656,7 @@ jobs:
ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}
ENTERPRISE_LICENSE_RC1: ${{ secrets.ENTERPRISE_LICENSE_RC1 }}
QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
run: yarn test:integration --image "${ROCKETCHAT_IMAGE}"
report-coverage:

View File

@ -8,6 +8,22 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';
function qaseRunTitle(): string {
const title = ['Federation E2E Tests'];
if (process.env.PR_NUMBER) {
title.push(`PR ${process.env.PR_NUMBER}`);
}
if (process.env.GITHUB_RUN_ID) {
title.push(`Run ${process.env.GITHUB_RUN_ID}`);
}
title.push(new Date().toISOString());
return title.join(' - ');
}
export default {
preset: server.preset,
transformIgnorePatterns: [
@ -36,7 +52,10 @@ export default {
testops: {
api: { token: process.env.QASE_TESTOPS_JEST_API_TOKEN },
project: 'RC',
run: { complete: true },
run: {
title: qaseRunTitle(),
complete: true,
},
},
debug: true,
},

View File

@ -14,7 +14,7 @@
"lint:fix": "eslint src --fix",
"test": "jest",
"test:integration": "./tests/scripts/run-integration-tests.sh",
"testend-to-end": "IS_EE=true NODE_EXTRA_CA_CERTS=$(pwd)/docker-compose/traefik/certs/ca/rootCA.crt jest --config jest.config.federation.ts --forceExit --testTimeout=30000",
"test:federation": "jest --config jest.config.federation.ts",
"testunit": "jest",
"typecheck": "tsc --noEmit --skipLibCheck"
},

View File

@ -22,7 +22,7 @@ import { SynapseClient } from '../helper/synapse-client';
beforeAll(async () => {
// Create admin request config for RC1
rc1AdminRequestConfig = await getRequestConfig(
federationConfig.rc1.apiUrl,
federationConfig.rc1.url,
federationConfig.rc1.adminUser,
federationConfig.rc1.adminPassword,
);

View File

@ -30,7 +30,7 @@ import { SynapseClient } from '../helper/synapse-client';
beforeAll(async () => {
// Create admin request config for RC1
rc1AdminRequestConfig = await getRequestConfig(
federationConfig.rc1.apiUrl,
federationConfig.rc1.url,
federationConfig.rc1.adminUser,
federationConfig.rc1.adminPassword,
);
@ -48,7 +48,7 @@ import { SynapseClient } from '../helper/synapse-client';
// Create user1 request config for RC1
rc1User1RequestConfig = await getRequestConfig(
federationConfig.rc1.apiUrl,
federationConfig.rc1.url,
federationConfig.rc1.additionalUser1.username,
federationConfig.rc1.additionalUser1.password,
);
@ -84,7 +84,7 @@ import { SynapseClient } from '../helper/synapse-client';
beforeAll(async () => {
const user = { username: `user-${Date.now()}`, password: '123' };
createdUser = await createUser(user, rc1AdminRequestConfig);
userRequestConfig = await getRequestConfig(federationConfig.rc1.apiUrl, user.username, user.password);
userRequestConfig = await getRequestConfig(federationConfig.rc1.url, user.username, user.password);
});
afterAll(async () => {
@ -256,7 +256,7 @@ import { SynapseClient } from '../helper/synapse-client';
describe('Go to the composer and use the /invite slash command to add a federated user', () => {
it('It should not allow and show an error message', async () => {
// Set up DDP listener to catch ephemeral messages
const ddpListener = createDDPListener(federationConfig.rc1.apiUrl, rc1AdminRequestConfig);
const ddpListener = createDDPListener(federationConfig.rc1.url, rc1AdminRequestConfig);
// Connect to DDP and subscribe to ephemeral messages
await ddpListener.connect();

View File

@ -5,31 +5,22 @@
* Rocket.Chat instances, Matrix homeservers, and user credentials needed
* for end-to-end federation testing.
*/
export interface IFederationConfig {
rc1: {
apiUrl: string;
adminUser: string;
adminPassword: string;
adminMatrixUserId: string;
additionalUser1: {
username: string;
password: string;
matrixUserId: string;
};
};
hs1: {
url: string;
adminMatrixUserId: string;
type FederationServerConfig = {
url: string;
domain: string;
adminUser: string;
adminPassword: string;
adminMatrixUserId: string;
additionalUser1: {
username: string;
password: string;
homeserver: string;
adminUser: string;
adminPassword: string;
additionalUser1: {
username: string;
password: string;
matrixUserId: string;
};
matrixUserId: string;
};
};
export interface IFederationConfig {
rc1: FederationServerConfig;
hs1: FederationServerConfig;
}
/**
@ -40,13 +31,12 @@ export interface IFederationConfig {
* Throws an error if a required variable is missing or empty.
*
* @param name - The name of the environment variable for error messages
* @param value - The environment variable value (may be undefined)
* @param defaultValue - Optional default value to use if variable is not set
* @returns The validated value (either the env var or default)
* @throws Error if the variable is required but missing or empty
*/
function validateEnvVar(name: string, value: string | undefined, defaultValue?: string): string {
const finalValue = value || defaultValue;
function validateEnvVar(name: string, defaultValue?: string): string {
const finalValue = process.env[name] || defaultValue;
if (!finalValue || finalValue.trim() === '') {
throw new Error(`Required environment variable ${name} is not set or is empty`);
}
@ -65,45 +55,41 @@ function validateEnvVar(name: string, value: string | undefined, defaultValue?:
* @throws Error if any required configuration is missing or invalid
*/
function getFederationConfig(): IFederationConfig {
const rcDomain = validateEnvVar('FEDERATION_RC1_DOMAIN', 'rc1');
const rcAdminUser = validateEnvVar('FEDERATION_RC1_ADMIN_USER', 'admin');
const rcAdminPassword = validateEnvVar('FEDERATION_RC1_ADMIN_PASSWORD', 'admin');
const rcAdditionalUser1 = validateEnvVar('FEDERATION_RC1_ADDITIONAL_USER1', 'user2');
const rcAdditionalUser1Password = validateEnvVar('FEDERATION_RC1_ADDITIONAL_USER1_PASSWORD', 'user2pass');
const hs1Domain = validateEnvVar('FEDERATION_SYNAPSE_DOMAIN', 'hs1');
const hs1AdminUser = validateEnvVar('FEDERATION_SYNAPSE_ADMIN_USER', 'admin');
const hs1AdminPassword = validateEnvVar('FEDERATION_SYNAPSE_ADMIN_PASSWORD', 'admin');
const hs1AdditionalUser1 = validateEnvVar('FEDERATION_SYNAPSE_ADDITIONAL_USER1', 'alice');
const hs1AdditionalUser1Password = validateEnvVar('FEDERATION_SYNAPSE_ADDITIONAL_USER1_PASSWORD', 'alice');
return {
rc1: {
apiUrl: validateEnvVar('FEDERATION_RC1_API_URL', process.env.FEDERATION_RC1_API_URL, 'https://rc1'),
adminUser: validateEnvVar('FEDERATION_RC1_ADMIN_USER', process.env.FEDERATION_RC1_ADMIN_USER, 'admin'),
adminPassword: validateEnvVar('FEDERATION_RC1_ADMIN_PASSWORD', process.env.FEDERATION_RC1_ADMIN_PASSWORD, 'admin'),
adminMatrixUserId: validateEnvVar('FEDERATION_RC1_USER_ID', process.env.FEDERATION_RC1_USER_ID, '@admin:rc1'),
url: `https://${rcDomain}`,
domain: rcDomain,
adminUser: rcAdminUser,
adminPassword: rcAdminPassword,
adminMatrixUserId: `@${rcAdminUser}:${rcDomain}`,
additionalUser1: {
username: validateEnvVar('FEDERATION_RC1_ADDITIONAL_USER1', process.env.FEDERATION_RC1_ADDITIONAL_USER1, 'user2'),
password: validateEnvVar(
'FEDERATION_RC1_ADDITIONAL_USER1_PASSWORD',
process.env.FEDERATION_RC1_ADDITIONAL_USER1_PASSWORD,
'user2pass',
),
matrixUserId: validateEnvVar(
'FEDERATION_RC1_ADDITIONAL_USER1_MATRIX_ID',
process.env.FEDERATION_RC1_ADDITIONAL_USER1_MATRIX_ID,
'@user2:rc1',
),
username: rcAdditionalUser1,
password: rcAdditionalUser1Password,
matrixUserId: `@${rcAdditionalUser1}:${rcDomain}`,
},
},
hs1: {
url: validateEnvVar('FEDERATION_SYNAPSE_URL', process.env.FEDERATION_SYNAPSE_URL, 'https://hs1'),
adminMatrixUserId: validateEnvVar('FEDERATION_SYNAPSE_USER', process.env.FEDERATION_SYNAPSE_USER, '@admin:hs1'),
password: validateEnvVar('FEDERATION_SYNAPSE_PASSWORD', process.env.FEDERATION_SYNAPSE_PASSWORD, 'admin'),
homeserver: validateEnvVar('FEDERATION_SYNAPSE_HOMESERVER', process.env.FEDERATION_SYNAPSE_HOMESERVER, 'hs1'),
adminUser: validateEnvVar('FEDERATION_SYNAPSE_ADMIN_USER', process.env.FEDERATION_SYNAPSE_ADMIN_USER, 'admin'),
adminPassword: validateEnvVar('FEDERATION_SYNAPSE_ADMIN_PASSWORD', process.env.FEDERATION_SYNAPSE_ADMIN_PASSWORD, 'admin'),
url: `https://${hs1Domain}`,
domain: hs1Domain,
adminUser: hs1AdminUser,
adminMatrixUserId: `@${hs1AdminUser}:${hs1Domain}`,
adminPassword: hs1AdminPassword,
additionalUser1: {
username: validateEnvVar('FEDERATION_SYNAPSE_ADDITIONAL_USER1', process.env.FEDERATION_SYNAPSE_ADDITIONAL_USER1, 'alice'),
password: validateEnvVar(
'FEDERATION_SYNAPSE_ADDITIONAL_USER1_PASSWORD',
process.env.FEDERATION_SYNAPSE_ADDITIONAL_USER1_PASSWORD,
'alice',
),
matrixUserId: validateEnvVar(
'FEDERATION_SYNAPSE_ADDITIONAL_USER1_MATRIX_ID',
process.env.FEDERATION_SYNAPSE_ADDITIONAL_USER1_MATRIX_ID,
'@alice:hs1',
),
username: hs1AdditionalUser1,
password: hs1AdditionalUser1Password,
matrixUserId: `@${hs1AdditionalUser1}:${hs1Domain}`,
},
},
};

View File

@ -110,7 +110,7 @@ cleanup() {
echo "=========================================="
echo "CONTAINER LOGS (Test Failed)"
echo "=========================================="
echo ""
echo "ROCKET.CHAT (rc1) LOGS:"
echo "----------------------------------------"
@ -119,7 +119,7 @@ cleanup() {
else
echo " Rocket.Chat container not found or no logs"
fi
echo ""
echo "SYNAPSE (hs1) LOGS:"
echo "----------------------------------------"
@ -128,11 +128,11 @@ cleanup() {
else
echo " Synapse container not found or no logs"
fi
echo ""
echo "=========================================="
fi
if [ "$KEEP_RUNNING" = true ]; then
log_info "Keeping Docker containers running (--keep-running flag set)"
log_info "Services are available at:"
@ -158,12 +158,12 @@ cleanup() {
fi
log_success "Cleanup completed"
fi
# Remove temporary build directory if it exists
if [ -n "${BUILD_DIR:-}" ] && [ -d "$BUILD_DIR" ]; then
rm -rf "$BUILD_DIR" || true
fi
# Exit with the test result code
if [ -n "${TEST_EXIT_CODE:-}" ]; then
exit $TEST_EXIT_CODE
@ -186,21 +186,21 @@ fi
if [ "$USE_PREBUILT_IMAGE" = false ]; then
log_info "🚀 Building Rocket.Chat locally..."
log_info "====================================="
# Clean up any existing build
log_info "Cleaning up previous build..."
rm -rf "$BUILD_DIR"
# Build the project
log_info "Building packages from project root..."
cd "$ROCKETCHAT_ROOT"
yarn build
# Build the Meteor bundle (must be run from the meteor directory)
log_info "Building Meteor bundle..."
cd "$ROCKETCHAT_ROOT/apps/meteor"
METEOR_DISABLE_OPTIMISTIC_CACHING=1 meteor build --server-only --directory "$BUILD_DIR"
log_success "Build completed!"
else
log_info "🚀 Using pre-built image: $PREBUILT_IMAGE"
@ -289,7 +289,7 @@ wait_for_service() {
# Capture curl output and error for debugging
curl_output=$(curl -fsS --cacert "$ca_cert" --resolve "${host}:${port}:127.0.0.1" "$url" 2>&1)
curl_exit_code=$?
if [ $curl_exit_code -eq 0 ]; then
log_success "$name is ready!"
return 0
@ -328,8 +328,8 @@ fi
if [ "$NO_TEST" = false ]; then
log_info "Running end-to-end tests..."
cd "$PACKAGE_ROOT"
yarn testend-to-end
IS_EE=true NODE_EXTRA_CA_CERTS=$(pwd)/docker-compose/traefik/certs/ca/rootCA.crt yarn test:federation
TEST_EXIT_CODE=$?
else
log_info "No-test mode: skipping test execution"

View File

@ -51,43 +51,103 @@ function describeImpl(name: string, fn: () => void): void {
const currentTest = global.test;
// Wrap it() to automatically set suite at the very start
global.it = ((testName: any, fn?: any, timeout?: number) => {
// Handle qase-wrapped test names (qase returns a string)
if (typeof testName === 'string' && fn) {
return currentIt(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
// Handle cases where testName might be a number or other type
return currentIt(testName, fn, timeout);
}) as typeof global.it;
global.it = Object.assign(
(testName: any, fn?: any, timeout?: number) => {
// Handle qase-wrapped test names (qase returns a string)
if (typeof testName === 'string' && fn) {
return currentIt(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
// Handle cases where testName might be a number or other type
return currentIt(testName, fn, timeout);
},
{
skip: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
currentIt.skip(name, fn);
} finally {
suitePathStack.pop();
}
},
only: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
currentIt.only(name, fn);
} finally {
suitePathStack.pop();
}
},
todo: (name: string) => {
suitePathStack.push(name);
try {
currentIt.todo(name);
} finally {
suitePathStack.pop();
}
},
},
) as typeof global.it;
// Wrap test() to automatically set suite at the very start
global.test = ((testName: any, fn?: any, timeout?: number) => {
if (typeof testName === 'string' && fn) {
return currentTest(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
return currentTest(testName, fn, timeout);
}) as typeof global.test;
global.test = Object.assign(
(testName: any, fn?: any, timeout?: number) => {
if (typeof testName === 'string' && fn) {
return currentTest(
testName,
async () => {
// Set suite immediately at the start of the test
qase.suite(currentPath);
// Call the original test function and return the result
return fn();
},
timeout,
);
}
return currentTest(testName, fn, timeout);
},
{
skip: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
currentTest.skip(name, fn);
} finally {
suitePathStack.pop();
}
},
only: (name: string, fn: () => void) => {
suitePathStack.push(name);
try {
currentTest.only(name, fn);
} finally {
suitePathStack.pop();
}
},
todo: (name: string) => {
suitePathStack.push(name);
try {
currentTest.todo(name);
} finally {
suitePathStack.pop();
}
},
},
) as typeof global.test;
// Execute the describe block
fn();
try {
fn();
} catch (error) {
console.error('Error in describe block:', error);
}
// Restore previous wrappers
global.it = currentIt;