diff --git a/.env.dev01 b/.env.dev01 index 4035142..2737d39 100644 --- a/.env.dev01 +++ b/.env.dev01 @@ -1,30 +1,29 @@ -# MAS +# URLs MAS_URL=https://auth.dev01.tchap.incubateur.net -OTHER_MAS_URL=https://auth.dev02.tchap.incubateur.net -EXTERNAL_MAS_URL=https://auth.ext01.tchap.incubateur.net - -ELEMENT_URL=https://www.tchap.incubateur.net - -# SYNAPSE -MATRIX_URL=https://matrix.dev01.tchap.incubateur.net -OTHER_MATRIX_URL=https://matrix.dev02.tchap.incubateur.net -EXTERNAL_MATRIX_URL=https://matrix.ext01.tchap.incubateur.net - -# mailpit +KEYCLOAK_URL=https://identite-sandbox.proconnect.gouv.fr/users/start-sign-in +ELEMENT_URL=https://tchap.incubateur.net +BASE_URL=https://matrix.dev01.tchap.incubateur.net MAIL_URL=https://yop.tchap.incubateur.net + +# mailpit user credentials MAILPIT_USER=tchap +MAILPIT_PWD= + +# Keycloak Admin Credentials +#KEYCLOAK_ADMIN_USERNAME=admin +#KEYCLOAK_ADMIN_PASSWORD=admin +#KEYCLOAK_REALM=proconnect-mock # MAS Admin API Credentials -MAS_ADMIN_URL=https://auth.dev01.tchap.incubateur.net -OTHER_MAS_ADMIN_URL=https://auth.dev02.tchap.incubateur.net -EXTERNAL_MAS_ADMIN_URL=https://auth.ext01.tchap.incubateur.net +MAS_ADMIN_CLIENT_ID=01J44RKQYM4G3TNVANTMTDYTX6 +MAS_ADMIN_CLIENT_SECRET= # Test User Credentials TEST_USER_PREFIX=user.dev +TEST_USER_PASSWORD=Test123456sdksdfkljfs222! # Email domains for test users STANDARD_EMAIL_DOMAIN=yop1.tchap.incubateur.net -OTHER_EMAIL_DOMAIN=yop2.tchap.incubateur.net INVITED_EMAIL_DOMAIN=yopext.tchap.incubateur.net NOT_INVITED_EMAIL_DOMAIN=gmail.com WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com @@ -42,3 +41,4 @@ TEST_IN_PARALLEL=false #disable tls verification in nodeJS (required for axios to work with self signed certificate) NODE_TLS_REJECT_UNAUTHORIZED = '0' +SYNAPSE_ADMIN_TOKEN= \ No newline at end of file diff --git a/.env.dev02 b/.env.dev02 index 571911d..d5f2e04 100644 --- a/.env.dev02 +++ b/.env.dev02 @@ -1,30 +1,30 @@ -# MAS +# URLs MAS_URL=https://auth.dev02.tchap.incubateur.net -OTHER_MAS_URL=https://auth.dev01.tchap.incubateur.net -EXTERNAL_MAS_URL=https://auth.ext01.tchap.incubateur.net - -ELEMENT_URL=https://www.tchap.incubateur.net +KEYCLOAK_URL=https://identite-sandbox.proconnect.gouv.fr/users/start-sign-in +ELEMENT_URL=https://tchap.incubateur.net +#ELEMENT_URL=http://localhost:8088 +BASE_URL=https://matrix.dev02.tchap.incubateur.net +MAIL_URL=https://yop.tchap.incubateur.net -# SYNAPSE -MATRIX_URL=https://matrix.dev02.tchap.incubateur.net -OTHER_MATRIX_URL=https://matrix.dev01.tchap.incubateur.net -EXTERNAL_MATRIX_URL=https://matrix.ext01.tchap.incubateur.net +# mailpit user credentials +MAILPIT_USER=tchap +MAILPIT_PWD= -# mailpit -MAIL_URL=https://yop.tchap.incubateur.net -MAILPIT_USER=tchap +# Keycloak Admin Credentials +#KEYCLOAK_ADMIN_USERNAME=admin +#KEYCLOAK_ADMIN_PASSWORD=admin +#KEYCLOAK_REALM=proconnect-mock -# MAS Admin URLs -MAS_ADMIN_URL=https://auth.dev02.tchap.incubateur.net -OTHER_MAS_ADMIN_URL=https://auth.dev01.tchap.incubateur.net -EXTERNAL_MAS_ADMIN_URL=https://auth.ext01.tchap.incubateur.net +# MAS Admin API Credentials +MAS_ADMIN_CLIENT_ID=01J44RKQYM4G3TNVANTMTDYTX6 +MAS_ADMIN_CLIENT_SECRET= # Test User Credentials TEST_USER_PREFIX=user.dev +TEST_USER_PASSWORD=Test123456sdksdfkljfs222! # Email domains for test users STANDARD_EMAIL_DOMAIN=yop2.tchap.incubateur.net -OTHER_EMAIL_DOMAIN=yop1.tchap.incubateur.net INVITED_EMAIL_DOMAIN=yopext.tchap.incubateur.net NOT_INVITED_EMAIL_DOMAIN=gmail.com WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com @@ -36,9 +36,11 @@ BROWSER_LOCALE=fr-FR # Screenshots Directory SCREENSHOTS_DIR=playwright-results/dev02 + #Run tests in parallel TEST_IN_PARALLEL=false #disable tls verification in nodeJS (required for axios to work with self signed certificate) NODE_TLS_REJECT_UNAUTHORIZED = '0' +SYNAPSE_ADMIN_TOKEN= \ No newline at end of file diff --git a/.env.int01 b/.env.int01 index d6f2e79..6393bbd 100644 --- a/.env.int01 +++ b/.env.int01 @@ -1,32 +1,31 @@ -# MAS +#int01 (preprod) + +# URLs MAS_URL=https://auth.i.tchap.gouv.fr -OTHER_MAS_URL=https://auth.a.tchap.gouv.fr -EXTERNAL_MAS_URL=https://auth.e.tchap.gouv.fr +#KEYCLOAK_URL=https://identite-sandbox.proconnect.gouv.fr/users/start-sign-in ELEMENT_URL=https://www.beta.tchap.gouv.fr - -# SYNAPSE -MATRIX_URL=https://matrix.i.tchap.gouv.fr -OTHER_MATRIX_URL=https://matrix.a.tchap.gouv.fr -EXTERNAL_MATRIX_URL=https://matrix.e.tchap.gouv.fr - -# mailpit +BASE_URL=https://matrix.i.tchap.gouv.fr MAIL_URL=https://yop.tchap.incubateur.net + +# mailpit user credentials MAILPIT_USER=tchap +MAILPIT_PWD= + +# Keycloak Admin Credentials +#KEYCLOAK_ADMIN_USERNAME=admin +#KEYCLOAK_ADMIN_PASSWORD=admin +#KEYCLOAK_REALM=proconnect-mock # MAS Admin API Credentials -MAS_ADMIN_URL= MAS_ADMIN_CLIENT_ID= -OTHER_MAS_ADMIN_URL= -OTHER_MAS_ADMIN_CLIENT_ID= -EXTERNAL_MAS_ADMIN_URL= -EXTERNAL_MAS_ADMIN_CLIENT_ID= +MAS_ADMIN_CLIENT_SECRET= # Test User Credentials TEST_USER_PREFIX=user.int01 +TEST_USER_PASSWORD=Test@123456sdksdfkljfs222 # Email domains for test users STANDARD_EMAIL_DOMAIN=yop1.tchap.incubateur.net -OTHER_EMAIL_DOMAIN=yop2.tchap.incubateur.net INVITED_EMAIL_DOMAIN=yopext.tchap.incubateur.net NOT_INVITED_EMAIL_DOMAIN=gmail.com WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com diff --git a/.env.int02 b/.env.int02 index 659a18c..edae58d 100644 --- a/.env.int02 +++ b/.env.int02 @@ -1,34 +1,31 @@ -# MAS -MAS_URL=https://auth.a.tchap.gouv.fr -OTHER_MAS_URL=https://auth.i.tchap.gouv.fr -EXTERNAL_MAS_URL=https://auth.e.tchap.gouv.fr +#int01 (preprod) +# URLs +MAS_URL=https://auth.a.tchap.gouv.fr +#KEYCLOAK_URL=https://identite-sandbox.proconnect.gouv.fr/users/start-sign-in ELEMENT_URL=https://www.beta.tchap.gouv.fr - -# SYNAPSE -MATRIX_URL=https://matrix.a.tchap.gouv.fr -OTHER_MATRIX_URL=https://matrix.i.tchap.gouv.fr -EXTERNAL_MATRIX_URL=https://matrix.e.tchap.gouv.fr - -# mailpit +BASE_URL=https://matrix.a.tchap.gouv.fr MAIL_URL=https://yop.tchap.incubateur.net + +# mailpit user credentials MAILPIT_USER=tchap +MAILPIT_PWD= + +# Keycloak Admin Credentials +#KEYCLOAK_ADMIN_USERNAME=admin +#KEYCLOAK_ADMIN_PASSWORD=admin +#KEYCLOAK_REALM=proconnect-mock # MAS Admin API Credentials -MAS_ADMIN_URL= MAS_ADMIN_CLIENT_ID= -OTHER_MAS_ADMIN_URL= -OTHER_MAS_ADMIN_CLIENT_ID= -EXTERNAL_MAS_ADMIN_URL= -EXTERNAL_MAS_ADMIN_CLIENT_ID= +MAS_ADMIN_CLIENT_SECRET= # Test User Credentials TEST_USER_PREFIX=user.int02 - +TEST_USER_PASSWORD=Test@123456sdksdfkljfs222 # Email domains for test users STANDARD_EMAIL_DOMAIN=yop2.tchap.incubateur.net -OTHER_EMAIL_DOMAIN=yop1.tchap.incubateur.net INVITED_EMAIL_DOMAIN=yopext.tchap.incubateur.net NOT_INVITED_EMAIL_DOMAIN=gmail.com WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com diff --git a/.env.local b/.env.local index c8aeb55..89790d8 100644 --- a/.env.local +++ b/.env.local @@ -1,35 +1,29 @@ -# MAS +# URLs MAS_URL=https://auth.tchapgouv.com -OTHER_MAS_URL= -EXTERNAL_MAS_URL= - KEYCLOAK_URL=https://sso.tchapgouv.com ELEMENT_URL=https://element.tchapgouv.com - -# SYNAPSE -MATRIX_URL=https://matrix.tchapgouv.com -OTHER_MATRIX_URL= -EXTERNAL_MATRIX_URL= - -# mailpit +BASE_URL=https://matrix.tchapgouv.com MAIL_URL=https://mail.tchapgouv.com + +# mailpit user credentials MAILPIT_USER = +MAILPIT_PWD = # Keycloak Admin Credentials KEYCLOAK_ADMIN_USERNAME=admin +KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_REALM=proconnect-mock # MAS Admin API Credentials -MAS_ADMIN_URL=https://auth.tchapgouv.com -OTHER_MAS_ADMIN_URL= -EXTERNAL_MAS_ADMIN_URL= +MAS_ADMIN_CLIENT_ID=01J44RKQYM4G3TNVANTMTDYTX6 +MAS_ADMIN_CLIENT_SECRET= # Test User Credentials TEST_USER_PREFIX=user.local +TEST_USER_PASSWORD=Test@123456azeErt!! # Email domains for test users STANDARD_EMAIL_DOMAIN=tchapgouv.com -OTHER_EMAIL_DOMAIN=tchapgouv.com INVITED_EMAIL_DOMAIN=invited.externe.com NOT_INVITED_EMAIL_DOMAIN=not.invited.externe.com WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com @@ -47,3 +41,4 @@ TEST_IN_PARALLEL=false #disable tls verification in nodeJS (required for axios to work with self signed certificate) NODE_TLS_REJECT_UNAUTHORIZED = '0' +SYNAPSE_ADMIN_TOKEN= \ No newline at end of file diff --git a/.env.localdev01 b/.env.localdev01 deleted file mode 100644 index 27668dd..0000000 --- a/.env.localdev01 +++ /dev/null @@ -1,49 +0,0 @@ -# MAS -MAS_URL=https://auth.dev01.tchap.incubateur.net -OTHER_MAS_URL=https://auth.dev02.tchap.incubateur.net -EXTERNAL_MAS_URL=https://auth.ext01.tchap.incubateur.net - -ELEMENT_URL=http://localhost:8088 - -# SYNAPSE -MATRIX_URL=https://matrix.dev01.tchap.incubateur.net -OTHER_MATRIX_URL=https://matrix.dev02.tchap.incubateur.net -EXTERNAL_MATRIX_URL=https://matrix.ext01.tchap.incubateur.net - -# mailpit -MAIL_URL=https://yop.tchap.incubateur.net -MAILPIT_USER=tchap - -# Keycloak Admin Credentials -#KEYCLOAK_ADMIN_USERNAME=admin -#KEYCLOAK_ADMIN_PASSWORD=admin -#KEYCLOAK_REALM=proconnect-mock - -# MAS Admin API Credentials -MAS_ADMIN_URL=https://auth.dev01.tchap.incubateur.net -OTHER_MAS_ADMIN_URL=https://auth.dev02.tchap.incubateur.net -EXTERNAL_MAS_ADMIN_URL=https://auth.ext01.tchap.incubateur.net - -# Test User Credentials -TEST_USER_PREFIX=user.dev - -# Email domains for test users -STANDARD_EMAIL_DOMAIN=yop1.tchap.incubateur.net -OTHER_EMAIL_DOMAIN=yop2.tchap.incubateur.net -INVITED_EMAIL_DOMAIN=yopext.tchap.incubateur.net -NOT_INVITED_EMAIL_DOMAIN=gmail.com -WRONG_SERVER_EMAIL_DOMAIN=wrong.server.com -NUMERIQUE_EMAIL_DOMAIN=numerique.gouv.fr - -# Browser Locale -BROWSER_LOCALE=fr-FR - -# Screenshots Directory -SCREENSHOTS_DIR=playwright-results/dev01 - -#Run tests in parallel -TEST_IN_PARALLEL=false - -#disable tls verification in nodeJS (required for axios to work with self signed certificate) -NODE_TLS_REJECT_UNAUTHORIZED = '0' - diff --git a/.env.sample b/.env.sample index 3b89b46..dddb604 100644 --- a/.env.sample +++ b/.env.sample @@ -1,19 +1,13 @@ -# MAS +# URLs MAS_URL=https://auth.tchapgouv.com -OTHER_MAS_URL=https://auth.tchapgouv.com -EXTERNAL_MAS_URL= - KEYCLOAK_URL=https://sso.tchapgouv.com ELEMENT_URL=https://element.tchapgouv.com - -# SYNAPSE -MATRIX_URL=https://matrix.tchapgouv.com -OTHER_MATRIX_URL= -EXTERNAL_MATRIX_URL= - -# mailpit +BASE_URL=https://matrix.tchapgouv.com MAIL_URL=https://mail.tchapgouv.com + +# mailpit user credentials MAILPIT_USER = +MAILPIT_PWD = # Keycloak Admin Credentials KEYCLOAK_ADMIN_USERNAME=admin @@ -21,25 +15,19 @@ KEYCLOAK_ADMIN_PASSWORD= KEYCLOAK_REALM=proconnect-mock # MAS Admin API Credentials -MAS_ADMIN_URL= MAS_ADMIN_CLIENT_ID=01J44RKQYM4G3TNVANTMTDYTX6 -OTHER_MAS_ADMIN_URL= -OTHER_MAS_ADMIN_CLIENT_ID= -EXTERNAL_MAS_ADMIN_URL= -EXTERNAL_MAS_ADMIN_CLIENT_ID= +MAS_ADMIN_CLIENT_SECRET= + # Test User Credentials TEST_USER_PREFIX=playwright_test_user - -#TEST_USER_PASSWORD= must be defined in .secrets.env - +TEST_USER_PASSWORD=Test@123456 # Email domains for test users -STANDARD_EMAIL_DOMAIN= -OTHER_EMAIL_DOMAIN= -INVITED_EMAIL_DOMAIN= -NOT_INVITED_EMAIL_DOMAIN= -WRONG_SERVER_EMAIL_DOMAIN= -NUMERIQUE_EMAIL_DOMAIN= +STANDARD_EMAIL_DOMAIN=incubateur.net +INVITED_EMAIL_DOMAIN=invited.yopmail.com +NOT_INVITED_EMAIL_DOMAIN=yopmail.com +WRONG_SERVER_EMAIL_DOMAIN=agent2.incubateur.net +NUMERIQUE_EMAIL_DOMAIN=numerique.gouv.fr # Screenshots Directory SCREENSHOTS_DIR=playwright-results diff --git a/.gitignore b/.gitignore index 065a432..ccdddcd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,3 @@ build/ # OS .DS_Store Thumbs.db - -# secrets -.secrets.* -!.secrets.sample \ No newline at end of file diff --git a/.secrets.sample b/.secrets.sample deleted file mode 100644 index 590c815..0000000 --- a/.secrets.sample +++ /dev/null @@ -1,17 +0,0 @@ -# MAS Admin API Secrets -# These secrets are used for authentication with the Matrix Authentication Service -# Each environment (dev, int, local) should have its own specific secrets -MAS_ADMIN_CLIENT_ID= -MAS_ADMIN_CLIENT_SECRET= -OTHER_MAS_ADMIN_CLIENT_ID= -OTHER_MAS_ADMIN_SECRET= -EXTERNAL_MAS_ADMIN_CLIENT_ID= -EXTERNAL_MAS_ADMIN_SECRET= - -# Mailpit Secrets -# Mailpit password for accessing the mailpit service -MAILPIT_PWD= - -# Synapse Admin Token -# Token for administrative access to Synapse -SYNAPSE_ADMIN_TOKEN= diff --git a/README.md b/README.md index a07c297..1e94478 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,20 @@ # Playwright Tests for Matrix Authentication Service -Ce projet contient differents tests : +Ce projet contient des tests Playwright pour tester le scénario d'authentification OIDC avec Keycloak dans le service d'authentification Matrix (MAS). -`/auth` : tests Playwright pour tester le scénario d'authentification OIDC avec Keycloak dans le service d'authentification Matrix (MAS). Nécessite un environnement local docker avec les services mockés de https://github.com/tchapgouv/tchap-docker-integration. +Cela contient également un script complet avec les tests minimaux Tchap -`/integration/api/room-access-rules` : tests d'intégration API pour le module room-access-rules. Nécessite un environnement Tchap complet. +## Démarrage des services en local +TODO -`/integration/api/email-account-validity` : scénario de recette web pour le module remail-account-validity. Nécessite un environnement Tchap complet et un service mailpit - -`/integration/web/minimal` : scénario minimal de recette e2e sur navigateur sur les serveurs de DEV (dev01, ext01) et PREPROD (int01, ext01). Nécessite un environnement Tchap complet avec un serveur agent et un serveur externe, ainsi qu'un service mailpit. - - -## Configuration - -Les tests utilisent deux types de fichiers pour la configuration d'un environnement : - -- `.env.XXX` - Fichier de configuration principal (non sensible) -- `.secrets.XXX` - Fichier de secrets (sensible, exclu de github) - -Vous pouvez modifier le fichier `.env.XXX` pour adapter les tests à votre environnement. Les variables sensibles doivent être placées dans le fichier `.secrets.XXX` correspondant. - -### Variables de configuration - -Les variables suivantes sont requises pour dev01 et int01 et doivent être placées dans les fichiers `.secrets.XXX` : - -- `MAILPIT_PWD=` mot de passe mailpit -- `MAS_ADMIN_CLIENT_ID=` identifiant client admin du MAS -- `MAS_ADMIN_CLIENT_SECRET=` secret client admin du MAS -- `SYNAPSE_ADMIN_TOKEN=` token admin de synapse -- `OTHER_MAS_ADMIN_CLIENT_ID=` identifiant client admin du MAS secondaire -- `OTHER_MAS_ADMIN_SECRET=` secret client admin du MAS secondaire -- `EXTERNAL_MAS_ADMIN_CLIENT_ID=` identifiant client admin du MAS externe -- `EXTERNAL_MAS_ADMIN_SECRET=` secret client admin du MAS externe -- `TEST_USER_PASSWORD=` mot de passe par défaut des users créés - -### Fichiers d'exemple - -Un fichier `.secrets.sample` est fourni avec des commentaires en anglais expliquant chaque variable. Vous pouvez copier ce fichier pour créer vos propres fichiers de secrets : +## Executer les tests minimaux avec docker ```bash -cp .secrets.sample .secrets.dev01 -cp .secrets.sample .secrets.int01 -# etc... +docker run -it --rm --ipc=host -v .:/app -w /app mcr.microsoft.com/playwright:v1.59.1-noble npm run test:dev01 +docker run -it --rm --ipc=host -v .:/app -w /app mcr.microsoft.com/playwright:v1.59.1-noble npm run test:int01 ``` -Les fichiers `.secrets.*` sont automatiquement exclus du contrôle de version par le fichier `.gitignore`. Ne commitez jamais de fichiers de secrets contenant des informations sensibles. - -## Executer les tests - -### avec docker -```bash -# specific test -docker run -it --rm -v .:/app -w /app mcr.microsoft.com/playwright:v1.59.1-noble bash -c 'ENV=dev01 npx playwright test ./tests/integration/api/room-access-rules --grep "Should create private encrypted room with correct properties"' - -# tests suite -docker run -it --rm -v .:/app -w /app mcr.microsoft.com/playwright:v1.59.1-noble npm run test:api:dev01 -``` - - -### Installation local +## Installation local Pour initialiser rapidement le projet, utilisez le script d'initialisation : @@ -68,13 +24,24 @@ chmod +x init.sh # Exécuter le script d'initialisation ./init.sh +``` -# tests suite -npm run test:room:dev01 +## Configuration -# specific test -ENV=dev01 && npx playwright test ./tests/integration/api/room-access-rules --grep "Should create private encrypted room with correct properties" -``` +Les tests utilisent un fichier `.env` pour la configuration. Vous pouvez modifier ce fichier pour adapter les tests à votre environnement. + +Requis pour dev01 et int01 (preprod): +`MAILPIT_PWD=` mailpit password + +## Exécution les tests + +```bash +# Exécuter tous les tests MAS en local +ENV=local npm run test tests/auth --retries=2 +# Exécuter les tests minimaux sur dev ou int01 (preprod) +ENV=int01 npm run test tests/minimal +ENV=dev01 npm run test tests/minimal +``` diff --git a/fixtures/auth-fixture.ts b/fixtures/auth-fixture.ts index ae16da4..5d1ccab 100644 --- a/fixtures/auth-fixture.ts +++ b/fixtures/auth-fixture.ts @@ -5,10 +5,14 @@ import { type TestUser, TypeUser, populateLocalStorageWithCredentials, - Credentials, } from '../utils/auth-helpers'; import { disposeApiContext as disposeKeycloakApiContext } from '../utils/keycloak-admin'; -import { MasAdminClient } from '../utils/mas-admin'; +import { + createMasUserWithPassword, + deactivateMasUser, + disposeApiContext as disposeMasApiContext, + waitForMasUser, +} from '../utils/mas-admin'; import { generateTestUserData } from '../utils/auth-helpers'; import fs from 'node:fs'; import path from 'node:path'; @@ -20,10 +24,10 @@ import { NOT_INVITED_EMAIL_DOMAIN, WRONG_SERVER_EMAIL_DOMAIN, NUMERIQUE_EMAIL_DOMAIN, - MATRIX_URL, + BASE_URL, ELEMENT_URL, } from '../utils/config'; -import { loginWithNewUser, standardUserOptions } from '../tests/integration/api/room-access-rules/room-utils'; +import { ClientServerApi, type Credentials } from '../utils/api'; function generateUserDataFixture(domain: string) { return async ({}, use: (user: TestUser) => Promise) => { @@ -58,7 +62,7 @@ function createKeycloakUserFixture(domain: string) { console.log(`Cleaned up test user: ${user.username}`); } finally { // Dispose API contexts - await Promise.all([disposeKeycloakApiContext()]); + await Promise.all([disposeKeycloakApiContext(), disposeMasApiContext()]); console.log('API contexts disposed'); } }; @@ -129,13 +133,13 @@ async function authenticatedUserFixture( { page, userData: user, request }: { page: Page; userData: TestUser; request: any }, use: (credentials: Credentials) => Promise ) { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - - const matrixAPI = await loginWithNewUser(masAdminClient, standardUserOptions() ) + // 1. Register user + const userId = await createMasUserWithPassword(user.username, user.email, user.password); + const csAPI = new ClientServerApi(BASE_URL, request); - const credentials = matrixAPI.credentials; + await waitForMasUser(user.email); - //console.log(credentials); + const credentials = (await csAPI.loginUser(user.username, user.password)) as Credentials; // 2. Populate localStorage await populateLocalStorageWithCredentials(page, credentials); @@ -148,7 +152,7 @@ async function authenticatedUserFixture( await use(credentials); // Clean up, deactivate user - await masAdminClient.deactivateUser(matrixAPI.masId); + await deactivateMasUser(userId); console.log(`Cleaned up MAS user: ${user.username}`); } diff --git a/init.sh b/init.sh index f26b106..e49f311 100755 --- a/init.sh +++ b/init.sh @@ -15,4 +15,9 @@ echo "Installing Playwright browsers..." npx playwright install echo "Initialization complete!" +echo "" +echo "Next steps:" +echo "1. Start the services https://github.com/tchapgouv/tchap-docker-integration" +echo "2. Run the tests with: npm test" +echo "" echo "For more information, see the README.md file." diff --git a/package-lock.json b/package-lock.json index 419e8b4..13ee8c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,28 +8,15 @@ "name": "mas-playwright-tests", "version": "1.0.0", "license": "ISC", - "dependencies": { - "matrix-js-sdk": "^41.5.0", - "playwright": "1.59.1" - }, "devDependencies": { "@biomejs/biome": "^2.4.11", - "@playwright/test": "1.59.1", + "@playwright/test": "^1.40.0", "@types/node": "^20.10.0", "dotenv": "^16.3.1", "mailpit-api": "^1.5.4", "typescript": "^5.3.2" } }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@biomejs/biome": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.11.tgz", @@ -193,15 +180,6 @@ "node": ">=14.21.3" } }, - "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", - "integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==", - "license": "Apache-2.0", - "engines": { - "node": ">= 18" - } - }, "node_modules/@playwright/test": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", @@ -218,12 +196,6 @@ "node": ">=18" } }, - "node_modules/@types/events": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.17.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", @@ -233,12 +205,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/another-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==", - "license": "Apache-2.0" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -247,32 +213,17 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", - "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, - "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -300,15 +251,6 @@ "node": ">= 0.8" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -395,15 +337,6 @@ "node": ">= 0.4" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -446,6 +379,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -548,9 +482,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -560,40 +494,6 @@ "node": ">= 0.4" } }, - "node_modules/is-network-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", - "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, "node_modules/mailpit-api": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/mailpit-api/-/mailpit-api-1.5.4.tgz", @@ -617,46 +517,6 @@ "node": ">= 0.4" } }, - "node_modules/matrix-events-sdk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", - "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==", - "license": "Apache-2.0" - }, - "node_modules/matrix-js-sdk": { - "version": "41.5.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz", - "integrity": "sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0", - "another-json": "^0.2.0", - "bs58": "^6.0.0", - "content-type": "^1.0.4", - "jwt-decode": "^4.0.0", - "loglevel": "^1.9.2", - "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.16.1", - "oidc-client-ts": "^3.0.1", - "p-retry": "8", - "sdp-transform": "^3.0.0", - "unhomoglyph": "^1.0.6" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/matrix-widget-api": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", - "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", - "license": "Apache-2.0", - "dependencies": { - "@types/events": "^3.0.0", - "events": "^3.2.0" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -680,37 +540,11 @@ "node": ">= 0.6" } }, - "node_modules/oidc-client-ts": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", - "integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==", - "license": "Apache-2.0", - "dependencies": { - "jwt-decode": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/p-retry": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", - "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", - "license": "MIT", - "dependencies": { - "is-network-error": "^1.3.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/playwright": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.59.1" @@ -729,6 +563,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -747,15 +582,6 @@ "node": ">=10" } }, - "node_modules/sdp-transform": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", - "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", - "license": "MIT", - "bin": { - "sdp-verify": "checker.js" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -774,12 +600,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true - }, - "node_modules/unhomoglyph": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", - "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==", - "license": "MIT" } } } diff --git a/package.json b/package.json index 47a6424..633a29b 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,9 @@ "test:headed": "playwright test --headed", "test:debug": "playwright test --debug", "test:ui": "playwright test --ui", - "test:auth:local": "ENV=local playwright test ./tests/auth", - "test:minimal:local": "ENV=localdev01 playwright test ./tests/integration/web/minimal/", - "test:minimal:dev01": "ENV=dev01 playwright test ./tests/integration/web/minimal/", - "test:minimal:int01": "ENV=int01 playwright test ./tests/integration/web/minimal/", - "test:api:dev01": "ENV=dev01 playwright test ./tests/integration/api/", - "test:api:int01": "ENV=int01 playwright test ./tests/integration/api/", - "test:all": "playwright test", + "test:local": "ENV=local playwright test", + "test:dev01": "ENV=dev01 playwright test ./tests/minimal/", + "test:int01": "ENV=int01 playwright test ./tests/minimal/", "lint": "biome lint .", "lint:fix": "biome lint . --fix", "format": "biome format .", @@ -28,13 +24,9 @@ ], "author": "", "license": "ISC", - "dependencies": { - "matrix-js-sdk": "^41.5.0", - "playwright": "1.59.1" - }, "devDependencies": { "@biomejs/biome": "^2.4.11", - "@playwright/test": "1.59.1", + "@playwright/test": "^1.40.0", "@types/node": "^20.10.0", "dotenv": "^16.3.1", "mailpit-api": "^1.5.4", diff --git a/playwright.config.ts b/playwright.config.ts index 8cda6aa..81a0c88 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,10 +17,10 @@ export default defineConfig({ /* Define how many workers */ // Limit the number of workers on CI, use default locally - workers: process.env.CI ? 1 : 1, + workers: process.env.CI ? 2 : 1, /* use retries to handle flaky tests */ - retries: process.env.CI ? 1 : 0, + retries: process.env.CI ? 5 : 2, /* Reporter to use */ reporter: 'html', @@ -47,29 +47,19 @@ export default defineConfig({ /* Configure projects for major browsers */ projects: [ - /* e2e tests do not work well on firefox nor webkit (bit flaky) */ - { + /* e2e tests do not work well on firefox nor webkit (bit flaky) + { name: 'firefox', - use: { - ...devices['Desktop Firefox'] }, - }, - /* + use: { ...devices['Desktop Firefox'] }, + }, { name: 'webkit', - use: { - ...devices['Desktop Edge'] }, - //NotAllowedError: Failed to execute 'readText' on 'Clipboard': Read permission denied. + use: { ...devices['Desktop Edge'] }, }, */ - { name: 'chromium', - use: { - contextOptions: { - permissions: ['clipboard-read', 'clipboard-write'], - }, - ...devices['Desktop Chrome'], - } + use: { ...devices['Desktop Chrome'] }, }, ], }); diff --git a/tests/auth/logout/tchap-logout.spec.ts b/tests/auth/logout/tchap-logout.spec.ts index b418f42..207d859 100644 --- a/tests/auth/logout/tchap-logout.spec.ts +++ b/tests/auth/logout/tchap-logout.spec.ts @@ -1,11 +1,10 @@ import { test, expect } from '../../../fixtures/auth-fixture'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { createMasUserWithPassword } from '../../../utils/mas-admin'; import { loginWithPassword } from '../../../utils/auth-helpers'; test.describe('Tchap : logout', () => { test('tchap login-logout-login', async ({ page, userData, screenChecker }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - userData.masId = await masAdminClient.createUserWithPassword( + userData.masId = await createMasUserWithPassword( userData.username, userData.email, userData.password @@ -42,9 +41,7 @@ test.describe('Tchap : logout', () => { await page.getByRole('button').filter({ hasText: 'Continuer' }).click(); // Success - Confirm identity - await expect( - page.getByRole('button').filter({ hasText: 'Vérification impossible ?' }) - ).toBeVisible({ timeout: 20000 }); + await expect(page.getByRole('button').filter({hasText: 'Vérification impossible ?'})).toBeVisible({ timeout: 20000 }); await screenChecker(page, `/`); }); }); diff --git a/tests/auth/oidc/mas-login-oidc.spec.ts b/tests/auth/oidc/mas-login-oidc.spec.ts index e54e3a8..6ddd2ed 100644 --- a/tests/auth/oidc/mas-login-oidc.spec.ts +++ b/tests/auth/oidc/mas-login-oidc.spec.ts @@ -4,7 +4,13 @@ import { performOidcLogin, type TestUser, } from '../../../utils/auth-helpers'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { + createMasUserWithPassword, + getMasUserByEmail, + deactivateMasUser, + oauthLinkExistsByUserId, + oauthLinkExistsBySubject, +} from '../../../utils/mas-admin'; import { SCREENSHOTS_DIR, STANDARD_EMAIL_DOMAIN } from '../../../utils/config'; test.describe('MAS Login OIDC', () => { @@ -13,12 +19,8 @@ test.describe('MAS Login OIDC', () => { // Create a user in MAS with the same email as the Keycloak user console.log(`Creating MAS user with same email as Keycloak user: ${oidcUser.email}`); - const masAdminClient = await MasAdminClient.createDefaultMAS(); - oidcUser.masId = await masAdminClient.createUserWithPassword( - oidcUser.username, - oidcUser.email, - 'any' - ); + + oidcUser.masId = await createMasUserWithPassword(oidcUser.username, oidcUser.email, 'any'); try { // Perform the OIDC login flow @@ -34,14 +36,15 @@ test.describe('MAS Login OIDC', () => { }); // Verify the user in MAS is still the same (account was linked, not created new) - const userAfterLogin = await masAdminClient.getUserByEmail(oidcUser.email); + const userAfterLogin = await getMasUserByEmail(oidcUser.email); expect(userAfterLogin.id).toBe(oidcUser.masId); - expect(await masAdminClient.oauthLinkExistsBySubject(oidcUser.username)).toBe(true); + //expect(await oauthLinkExistsByUserId(userLegacy.masId)).toBe(true); + expect(await oauthLinkExistsBySubject(oidcUser.username)).toBe(true); console.log(`Successfully verified account linking for user with email: ${oidcUser.email}`); } finally { // Clean up the MAS user - await masAdminClient.deactivateUser(oidcUser.masId); + await deactivateMasUser(oidcUser.masId); console.log(`Cleaned up MAS user: ${oidcUser.username}`); } }); @@ -52,14 +55,13 @@ test.describe('MAS Login OIDC', () => { }) => { // we use the fixture oidcExternalUserWitoutInvit because as long as the account is created, // there is no invitation pending in the identity server. - const masAdminClient = await MasAdminClient.createDefaultMAS(); const screenshot_path = test.info().title.replace(' ', '_'); // Create a user in MAS with the same email as the Keycloak user console.log(`Creating MAS user with same email as Keycloak user: ${externalUser.email}`); - externalUser.masId = await masAdminClient.createUserWithPassword( + externalUser.masId = await createMasUserWithPassword( externalUser.username, externalUser.email, externalUser.password @@ -79,16 +81,17 @@ test.describe('MAS Login OIDC', () => { }); // Verify the user in MAS is still the same (account was linked, not created new) - const userAfterLogin = await masAdminClient.getUserByEmail(externalUser.email); + const userAfterLogin = await getMasUserByEmail(externalUser.email); expect(userAfterLogin.id).toBe(externalUser.masId); - expect(await masAdminClient.oauthLinkExistsBySubject(externalUser.username)).toBe(true); + //expect(await oauthLinkExistsByUserId(userLegacy.masId)).toBe(true); + expect(await oauthLinkExistsBySubject(externalUser.username)).toBe(true); console.log( `Successfully verified account linking for user with email: ${externalUser.email}` ); } finally { // Clean up the MAS user - await masAdminClient.deactivateUser(externalUser.masId); + await deactivateMasUser(externalUser.masId); console.log(`Cleaned up MAS user: ${externalUser.username}`); } }); @@ -97,8 +100,6 @@ test.describe('MAS Login OIDC', () => { page, oidcUserWithFallbackRules: oidcUser, }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const screenshot_path = test.info().title.replace(' ', '_'); const old_email_domain = '@beta.gouv.fr'; @@ -109,7 +110,7 @@ test.describe('MAS Login OIDC', () => { `Creating MAS user with old email: ${old_email} whereas email in keycloak is : ${oidcUser.email}` ); - oidcUser.masId = await masAdminClient.createUserWithPassword( + oidcUser.masId = await createMasUserWithPassword( `${oidcUser.username}different_from_email`, old_email, oidcUser.password @@ -129,21 +130,19 @@ test.describe('MAS Login OIDC', () => { }); // Verify the user in MAS is still the same (account was linked, not created new) - const userAfterLogin = await masAdminClient.getUserByEmail(old_email); + const userAfterLogin = await getMasUserByEmail(old_email); expect(userAfterLogin.id).toBe(oidcUser.masId); - expect(await masAdminClient.oauthLinkExistsByUserId(oidcUser.masId)).toBe(true); + expect(await oauthLinkExistsByUserId(oidcUser.masId)).toBe(true); console.log(`Successfully verified account linking for user with email: ${old_email}`); } finally { // Clean up the MAS user - await masAdminClient.deactivateUser(oidcUser.masId); + await deactivateMasUser(oidcUser.masId); console.log(`Cleaned up MAS user: ${oidcUser.username}`); } }); test('match account by username throw error when email does not match', async ({ page }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const screenshot_path = test.info().title.replace(' ', '_'); //create a user in keycloak with an `email` that matches a `localpart` in MAS @@ -159,18 +158,12 @@ test.describe('MAS Login OIDC', () => { const testUser: TestUser = { username: `test.user${randomSuffix}-${domain}`, email: `test.user${randomSuffix}@${domain}`, - displayName: `Test User${randomSuffix}`, - domain: domain, password: '1234!', }; const user = await createKeycloakTestUser(testUser); - user.masId = await masAdminClient.createUserWithPassword( - user.username, - mas_user_email, - user.password - ); + user.masId = await createMasUserWithPassword(user.username, mas_user_email, user.password); try { // Perform the OIDC login flow @@ -183,7 +176,7 @@ test.describe('MAS Login OIDC', () => { console.log(`Successfully verified account linking for user with email: ${user.email}`); } finally { // Clean up the MAS user - await masAdminClient.deactivateUser(user.masId); + await deactivateMasUser(user.masId); console.log(`Cleaned up MAS user: ${user.username}`); } }); diff --git a/tests/auth/oidc/mas-register-oidc.spec.ts b/tests/auth/oidc/mas-register-oidc.spec.ts index 34a10d7..94fa64a 100644 --- a/tests/auth/oidc/mas-register-oidc.spec.ts +++ b/tests/auth/oidc/mas-register-oidc.spec.ts @@ -1,15 +1,14 @@ import { test, expect } from '../../../fixtures/auth-fixture'; import { performOidcLogin, verifyUserInMas } from '../../../utils/auth-helpers'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { checkMasUserExistsByEmail } from '../../../utils/mas-admin'; import { SCREENSHOTS_DIR } from '../../../utils/config'; test.describe('MAS register OIDC', () => { test('mas register oidc - with allowed account', async ({ page, oidcUser }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); // Verify the test user doesn't exist in MAS yet - const existsBeforeLogin = await masAdminClient.checkUserExistsByEmail(oidcUser.email); + const existsBeforeLogin = await checkMasUserExistsByEmail(oidcUser.email); expect(existsBeforeLogin).toBe(false); // Perform the OIDC login flow @@ -23,10 +22,10 @@ test.describe('MAS register OIDC', () => { await page.screenshot({ path: `${SCREENSHOTS_DIR}/${screenshot_path}/04-authenticated.png` }); // Verify the user was created in MAS - await verifyUserInMas(oidcUser, masAdminClient); + await verifyUserInMas(oidcUser); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail(oidcUser.email); + const existsAfterLogin = await checkMasUserExistsByEmail(oidcUser.email); expect(existsAfterLogin).toBe(true); console.log( @@ -39,12 +38,9 @@ test.describe('MAS register OIDC', () => { oidcExternalUserWitoutInvit, }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); // Verify the test user doesn't exist in MAS yet - const existsBeforeLogin = await masAdminClient.checkUserExistsByEmail( - oidcExternalUserWitoutInvit.email - ); + const existsBeforeLogin = await checkMasUserExistsByEmail(oidcExternalUserWitoutInvit.email); expect(existsBeforeLogin).toBe(false); // Perform the OIDC login flow @@ -57,9 +53,7 @@ test.describe('MAS register OIDC', () => { await page.screenshot({ path: `${SCREENSHOTS_DIR}/${screenshot_path}/04-error-no-invit.png` }); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail( - oidcExternalUserWitoutInvit.email - ); + const existsAfterLogin = await checkMasUserExistsByEmail(oidcExternalUserWitoutInvit.email); expect(existsAfterLogin).toBe(false); console.log( @@ -72,12 +66,9 @@ test.describe('MAS register OIDC', () => { oidcExternalUserWithInvit, }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); // Verify the test user doesn't exist in MAS yet - const existsBeforeLogin = await masAdminClient.checkUserExistsByEmail( - oidcExternalUserWithInvit.email - ); + const existsBeforeLogin = await checkMasUserExistsByEmail(oidcExternalUserWithInvit.email); expect(existsBeforeLogin).toBe(false); // Perform the OIDC login flow @@ -92,12 +83,10 @@ test.describe('MAS register OIDC', () => { }); // Verify the user was created in MAS - await verifyUserInMas(oidcExternalUserWithInvit, masAdminClient); + await verifyUserInMas(oidcExternalUserWithInvit); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail( - oidcExternalUserWithInvit.email - ); + const existsAfterLogin = await checkMasUserExistsByEmail(oidcExternalUserWithInvit.email); expect(existsAfterLogin).toBe(true); console.log( @@ -107,12 +96,9 @@ test.describe('MAS register OIDC', () => { test('mas register oidc - on wrong homeserver', async ({ page, oidcUserOnWrongServer }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); // Verify the test user doesn't exist in MAS yet - const existsBeforeLogin = await masAdminClient.checkUserExistsByEmail( - oidcUserOnWrongServer.email - ); + const existsBeforeLogin = await checkMasUserExistsByEmail(oidcUserOnWrongServer.email); expect(existsBeforeLogin).toBe(false); // Perform the OIDC login flow @@ -127,9 +113,7 @@ test.describe('MAS register OIDC', () => { }); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail( - oidcUserOnWrongServer.email - ); + const existsAfterLogin = await checkMasUserExistsByEmail(oidcUserOnWrongServer.email); expect(existsAfterLogin).toBe(false); console.log( diff --git a/tests/auth/oidc/tchap-login-oidc.spec.ts b/tests/auth/oidc/tchap-login-oidc.spec.ts index 8740b82..774b02c 100644 --- a/tests/auth/oidc/tchap-login-oidc.spec.ts +++ b/tests/auth/oidc/tchap-login-oidc.spec.ts @@ -1,22 +1,21 @@ import { test, expect } from '../../../fixtures/auth-fixture'; import { verifyUserInMas, performOidcLoginFromTchap } from '../../../utils/auth-helpers'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { checkMasUserExistsByEmail, createMasUserWithPassword } from '../../../utils/mas-admin'; import { SCREENSHOTS_DIR } from '../../../utils/config'; //flaky on await expect(page.locator('text=Configuration')).toBeVisible({timeout: 20000}); test.describe('Tchap : Login via OIDC', () => { test('tchap match account by email', async ({ page, oidcUser }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); - oidcUser.masId = await masAdminClient.createUserWithPassword( + oidcUser.masId = await createMasUserWithPassword( oidcUser.username, oidcUser.email, oidcUser.password ); // Verify the test user doesn't exist in MAS yet - const existsBeforeLogin = await masAdminClient.checkUserExistsByEmail(oidcUser.email); + const existsBeforeLogin = await checkMasUserExistsByEmail(oidcUser.email); expect(existsBeforeLogin).toBe(true); // Perform the OIDC login flow @@ -34,10 +33,10 @@ test.describe('Tchap : Login via OIDC', () => { await page.screenshot({ path: `${SCREENSHOTS_DIR}/${screenshot_path}/06-auth-success.png` }); // Verify the user was created in MAS - await verifyUserInMas(oidcUser, masAdminClient); + await verifyUserInMas(oidcUser); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail(oidcUser.email); + const existsAfterLogin = await checkMasUserExistsByEmail(oidcUser.email); expect(existsAfterLogin).toBe(true); console.log( diff --git a/tests/auth/password/mas-legacy.spec.ts b/tests/auth/password/mas-legacy.spec.ts index b47c5b9..2a5e278 100644 --- a/tests/auth/password/mas-legacy.spec.ts +++ b/tests/auth/password/mas-legacy.spec.ts @@ -1,11 +1,11 @@ import { test, expect } from '@playwright/test'; -import { MATRIX_URL, MAS_URL } from '../../../utils/config'; +import { BASE_URL, MAS_URL } from '../../../utils/config'; test.describe('Tchap : Legacy SSO Flow', () => { test('verify legacy SSO redirect chain for registration', async ({ request }) => { // The initial legacy SSO URL const email = 'user@exemple.com'; - const initialUrl = `${MATRIX_URL}/_matrix/client/r0/login/sso/redirect?redirectUrl=tchap%3A%2F%2Fconnect&org.matrix.msc3824.action=REGISTER&login_hint=${encodeURIComponent(email)}`; + const initialUrl = `${BASE_URL}/_matrix/client/r0/login/sso/redirect?redirectUrl=tchap%3A%2F%2Fconnect&org.matrix.msc3824.action=REGISTER&login_hint=${encodeURIComponent(email)}`; //login_hint is not taken into account in MAS for compat-sso flow console.log(`[Legacy SSO] Step 1: Calling initial URL: ${initialUrl}`); diff --git a/tests/auth/password/mas-login-password.spec.ts b/tests/auth/password/mas-login-password.spec.ts index 16c796e..723e365 100644 --- a/tests/auth/password/mas-login-password.spec.ts +++ b/tests/auth/password/mas-login-password.spec.ts @@ -5,15 +5,13 @@ import { performPasswordLogin, } from '../../../utils/auth-helpers'; import { SCREENSHOTS_DIR } from '../../../utils/config'; -import { MasAdminClient } from '../../../utils/mas-admin'; test.describe('MAS login password', () => { test('with allowed account', async ({ page }) => { const screenshot_path = test.info().title.replace(' ', '_'); // Create a test user with a password in MAS - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const user = await createMasTestUser('exemple.com', masAdminClient); + const user = await createMasTestUser('exemple.com'); try { console.log(`Created test user in MAS: ${user.username} (${user.email})`); @@ -32,7 +30,7 @@ test.describe('MAS login password', () => { console.log(`Successfully authenticated with password for user: ${user.username}`); } finally { // Clean up the test user - await cleanupMasTestUser(user, masAdminClient); + await cleanupMasTestUser(user); console.log(`Cleaned up test user: ${user.username}`); } }); diff --git a/tests/auth/password/tchap-login-password.spec.ts b/tests/auth/password/tchap-login-password.spec.ts index 3116934..63c8815 100644 --- a/tests/auth/password/tchap-login-password.spec.ts +++ b/tests/auth/password/tchap-login-password.spec.ts @@ -1,17 +1,18 @@ import { test, expect } from '../../../fixtures/auth-fixture'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { checkMasUserExistsByEmail, createMasUserWithPassword } from '../../../utils/mas-admin'; import { SCREENSHOTS_DIR, ELEMENT_URL } from '../../../utils/config'; test.describe('Tchap : Login password', () => { test('tchap login with password and login_hint', async ({ page, userData, screenChecker }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); - userData.masId = await masAdminClient.createUserWithPassword( + userData.masId = await createMasUserWithPassword( userData.username, userData.email, userData.password ); + const existsBeforeLogin = await checkMasUserExistsByEmail(userData.email); + expect(existsBeforeLogin).toBe(true); await page.goto(`${ELEMENT_URL}/#/welcome`, { waitUntil: 'networkidle' }); @@ -37,7 +38,7 @@ test.describe('Tchap : Login password', () => { await page.screenshot({ path: `${SCREENSHOTS_DIR}/${screenshot_path}/05-auth-success.png` }); // Double-check with the API - const existsAfterLogin = await masAdminClient.checkUserExistsByEmail(userData.email); + const existsAfterLogin = await checkMasUserExistsByEmail(userData.email); expect(existsAfterLogin).toBe(true); console.log( diff --git a/tests/auth/password/tchap-register-password.spec.ts b/tests/auth/password/tchap-register-password.spec.ts index 6ee8a62..1aad532 100644 --- a/tests/auth/password/tchap-register-password.spec.ts +++ b/tests/auth/password/tchap-register-password.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '../../../fixtures/auth-fixture'; import { createMasTestUser, generateTestUserData } from '../../../utils/auth-helpers'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { getMasUserByEmail } from '../../../utils/mas-admin'; import { NOT_INVITED_EMAIL_DOMAIN, STANDARD_EMAIL_DOMAIN, @@ -44,8 +44,7 @@ test.describe('Tchap : register with password', () => { await expect(page.locator('h1').filter({ hasText: /Bienvenue.*\[Tchapgouv\]/ })).toBeVisible({ timeout: 20000, }); - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const created_user = await masAdminClient.getUserByEmail(user.email); + const created_user = await getMasUserByEmail(user.email); //check created username fields expect(created_user.attributes.username).toContain(user.username); @@ -127,10 +126,8 @@ test.describe('Tchap : register with password', () => { screenChecker: screen, startTchapRegisterWithEmail, }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - //const created_user = await masAdminClient.createUserWithPassword(); // Create a test user with a password in MAS with API - const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); + const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN); // Create a test user with a password in MAS with web flow /* diff --git a/tests/auth/password/tchap-reset-password.spec.ts b/tests/auth/password/tchap-reset-password.spec.ts index 528b49c..daef8d0 100644 --- a/tests/auth/password/tchap-reset-password.spec.ts +++ b/tests/auth/password/tchap-reset-password.spec.ts @@ -1,23 +1,17 @@ -import { MasAdminClient } from '../../../utils/mas-admin'; import { test, expect } from '../../../fixtures/auth-fixture'; +import { checkMasUserExistsByEmail, createMasUserWithPassword } from '../../../utils/mas-admin'; import { ELEMENT_URL } from '../../../utils/config'; import { openResetPasswordEmail } from '../../../utils/auth-helpers'; test.describe('Tchap : reset password', () => { test('tchap reset password', async ({ page, userData, screenChecker }) => { - /* userData.masId = await createMasUserWithPassword( userData.username, userData.email, userData.password ); - */ - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const masId = await masAdminClient.createUserWithPassword( - userData.username, - userData.email, - userData.password - ); + const existsBeforeLogin = await checkMasUserExistsByEmail(userData.email); + expect(existsBeforeLogin).toBe(true); await page.goto(`${ELEMENT_URL}/#/welcome`, { waitUntil: 'networkidle' }); @@ -63,8 +57,6 @@ test.describe('Tchap : reset password', () => { //first tab is stuck back to recovery page await screenChecker(page, '/recover/progress'); - - masAdminClient.deactivateUser(masId); }); //TODO add reset password when user is connected -> show an error diff --git a/tests/integration/api/manage-last-admin/manage-last-admin.spec.ts b/tests/integration/api/manage-last-admin/manage-last-admin.spec.ts deleted file mode 100644 index ab21f94..0000000 --- a/tests/integration/api/manage-last-admin/manage-last-admin.spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - createPrivateUnencryptedRoom, - createPrivateEncryptedRoom, - loginWithNewUser, - addNewUserToRoom, - addModeratorToRoom, - addAdminToRoom, - standardUserOptions, - externalUserOptions, -} from '../room-access-rules/room-utils'; -import { EventType } from 'matrix-js-sdk'; - -test.describe('API - Manage Last Admin', () => { - let matrix: MatrixApi; - let masId: string; - let masAdminClient: MasAdminClient; - let externalMasClient: MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - externalMasClient = await MasAdminClient.createExternalMAS(); - const user = await loginWithNewUser(masAdminClient, standardUserOptions()); - matrix = user.matrix; - masId = user.masId; - }); - - test('Scenario 1: Last admin leaves private room - users_default becomes 100', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - const user1 = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - - // Check initial state, users_default PL should be 0 - const initialPowerLevels = await matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - const initialUsersDefault = initialPowerLevels.users_default; - expect(initialUsersDefault).toBe(0); - - // Admin (mxId) leaves - await matrix.getClient().leave(roomId); - - // users_default PL should be 100 - const finalPowerLevels = await user1.matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - expect(finalPowerLevels.users_default).toBe(100); - - await masAdminClient.deactivateUser(user1.masId); - }); - - test('Scenario 2: Admin leaves - another admin remains - no changes', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - // Create and promote a second admin - const admin2 = await addAdminToRoom(matrix, roomId, masAdminClient); - - // Admin (mxId) leaves - await matrix.getClient().leave(roomId); - - // Check that users_default remains 0 (no change needed) - const finalPowerLevels = await admin2.matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - expect(finalPowerLevels.users_default).toBe(0); - - // Check that admin2 remains admin - expect(finalPowerLevels.users[admin2.mxId]).toBe(100); - - masAdminClient.deactivateUser(admin2.masId); - }); - - test('Scenario 3: Last admin leaves - moderator promoted to admin', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - const user = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - const moderator = await addModeratorToRoom(matrix, roomId, masAdminClient); - - // Admin (mxId) leaves - await matrix.getClient().leave(roomId); - - // Check that moderator is promoted to admin (PL=100) - const finalPowerLevels = await moderator.matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - expect(finalPowerLevels.users[moderator.mxId]).toBe(100); - - await masAdminClient.deactivateUser(moderator.masId); - await masAdminClient.deactivateUser(user.masId); - }); - - test('Scenario 4: Last admin leaves external room - internal users promoted', async () => { - // Create external room - const roomId = await matrix.createRoom({ - name: 'External Room', - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - accessRules: { - rule: 'unrestricted', - visibility: 'private', - }, - }); - - const internal1 = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - const external1 = await addNewUserToRoom( - matrix, - roomId, - externalMasClient, - externalUserOptions() - ); - - // Admin (mxId) leaves - await matrix.getClient().leave(roomId); - - // check PL for internal users - for (const u of [internal1]) { - const finalPowerLevels = await u.matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - expect(finalPowerLevels.users[u.mxId]).toBe(100); - } - - await masAdminClient.deactivateUser(internal1.masId); - await externalMasClient.deactivateUser(external1.masId); - }); - - test('Scenario 5: Last admin leaves external room - moderators promoted', async () => { - // Create external room - const roomId = await matrix.createRoom({ - name: 'External Room with Mods', - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - accessRules: { - rule: 'unrestricted', - visibility: 'private', - }, - }); - - const user = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - const mod1 = await addModeratorToRoom(matrix, roomId, masAdminClient); - const external1 = await addNewUserToRoom( - matrix, - roomId, - externalMasClient, - externalUserOptions() - ); - - // Admin (mxId) leaves - await matrix.getClient().leave(roomId); - - // Check that moderators are promoted to admin (PL=100) - const finalPowerLevels = await mod1.matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - expect(finalPowerLevels.users[mod1.mxId]).toBe(100); - - // clean - await masAdminClient.deactivateUser(mod1.masId); - await masAdminClient.deactivateUser(user.masId); - await externalMasClient.deactivateUser(external1.masId); - }); - - test('Scenario 6: Admin leaves an empty external unencrypted room - no error', async () => { - // Create empty an unencrypted room - const roomId = await createPrivateUnencryptedRoom(matrix); - - // Change the state of the room to invite an external - await matrix.sendStateEvent(roomId, AccessRulesEventType, { - rule: 'unrestricted', - visibility: 'private', - force_unencrypted_at_creation: true, - }); - - // Admin (mxId) leaves - should not throw error - await expect(matrix.getClient().leave(roomId)).resolves.toBeDefined(); - - // Check that room is no longer accessible to admin - const room = await matrix.getClient().getRoom(roomId); - expect(room).toBeNull(); - }); - - test('Scenario 7: Admin leaves an empty encrypted external room - no error', async () => { - // Create empty an unencrypted room - const roomId = await createPrivateEncryptedRoom(matrix); - - // Change the state of the room to invite an external - await matrix.sendStateEvent(roomId, AccessRulesEventType, { - rule: 'unrestricted', - visibility: 'private', - }); - - // Admin (mxId) leaves - should not throw error - await expect(matrix.getClient().leave(roomId)).resolves.toBeDefined(); - - // Check that room is no longer accessible to admin - const room = await matrix.getClient().getRoom(roomId); - expect(room).toBeNull(); - }); - - test.afterAll(async () => { - await masAdminClient.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/direct-room.spec.ts b/tests/integration/api/room-access-rules/direct-room.spec.ts deleted file mode 100644 index 624dc70..0000000 --- a/tests/integration/api/room-access-rules/direct-room.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { expectErrorWhenSendStateEvent, loginWithNewUser, standardUserOptions } from './room-utils'; -import { EventType, JoinRule } from 'matrix-js-sdk'; - -export async function createDirectRoom( - matrix: MatrixApi, - name: string = 'Direct Room' -): Promise { - return matrix.createRoom({ - name, - joinRule: 'invite', - preset: 'trusted_private_chat', - visibility: 'private', - encryption: false, - accessRules: { - rule: 'direct', - force_unencrypted_at_creation: false, - visibility: 'private', - }, - is_direct: true, - }); -} - -test.describe('API - Direct Room', () => { - let matrix: MatrixApi; - let mxId: string; - let masId: string; - let masAdmin: MasAdminClient; - - test.beforeAll(async () => { - masAdmin = await MasAdminClient.createDefaultMAS(); - const user = await loginWithNewUser(masAdmin, standardUserOptions()); - mxId = user.mxId; - matrix = user.matrix; - masId = user.masId; - }); - - test('Should create direct room with correct properties', async () => { - const roomId = await createDirectRoom(matrix); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('direct'); - expect(accessRules.force_unencrypted_at_creation).toBe(false); - expect(accessRules.visibility).toBe('private'); - expect(await matrix.isRoomEncrypted(roomId)).toBe(true); - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should return 403 error when changing joining rule', async () => { - const roomId = await createDirectRoom(matrix); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - EventType.RoomJoinRules, - { join_rule: JoinRule.Public }, - 403 - ); - }); - - test('Should return 403 error when changing access rules', async () => { - const roomId = await createDirectRoom(matrix); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'unrestricted' }, - 403 - ); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted' }, - 403 - ); - }); - - test.afterAll(async () => { - masAdmin.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/external-restriction.spec.ts b/tests/integration/api/room-access-rules/external-restriction.spec.ts deleted file mode 100644 index c414edf..0000000 --- a/tests/integration/api/room-access-rules/external-restriction.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { - createPrivateEncryptedRoom, - externalUserOptions, - loginWithNewUser, - standardUserOptions, -} from './room-utils'; -import { MasAdminClient } from '../../../../utils/mas-admin'; - -test.describe('API - External restriction', () => { - let externalUser: any; - let externalMasAdmin: MasAdminClient; - - test.beforeAll(async () => { - externalMasAdmin = await MasAdminClient.createExternalMAS(); - externalUser = await loginWithNewUser(externalMasAdmin, externalUserOptions()); - }); - - test('External users can not create rooms', async () => { - await expect(createPrivateEncryptedRoom(externalUser.matrix)).rejects.toMatchObject({ - httpStatus: 403, - }); - }); - - test('External users get empty results when searching user directory ', async () => { - //create new user to search for - const masAdmin = await MasAdminClient.createDefaultMAS(); - const user = await loginWithNewUser(masAdmin, standardUserOptions()); - - await expect( - externalUser.matrix.getClient().searchUserDirectory({ term: user.username }) - ).resolves.toMatchObject({ results: [] }); - - await masAdmin.deactivateUser(user.masId); - }); - - test.afterAll(async () => { - await externalMasAdmin.deactivateUser(externalUser.masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/federation.spec.ts b/tests/integration/api/room-access-rules/federation.spec.ts deleted file mode 100644 index 5f3a40c..0000000 --- a/tests/integration/api/room-access-rules/federation.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test, expect } from '@playwright/test'; -import type { MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - createPrivateUnencryptedRoom, - federatedUserOptions, - loginWithNewUser, - standardUserOptions, -} from './room-utils'; - -test.describe('API - Federation', () => { - let matrix: MatrixApi; - let mxId: string; - let masId: string; - let masAdmin: MasAdminClient; - - test.beforeAll(async () => { - masAdmin = await MasAdminClient.createDefaultMAS(); - const user = await loginWithNewUser(masAdmin, standardUserOptions()); - matrix = user.matrix; - masId = user.masId; - }); - - test('Should create room on federated server', async () => { - test.setTimeout(15000); - - const roomId = await createPrivateUnencryptedRoom(matrix); - const accessRules = await matrix.getAccessRulesEvent(roomId); - - const federatedMas = await MasAdminClient.createFederatedMAS(); - const federatedUser = await loginWithNewUser(federatedMas, federatedUserOptions()); - - //start sync - //await matrix.getClient().startClient(); //no need - - // Start the federatedUser client sync to read messages - await federatedUser.matrix.getClient().startClient(); - - // Invite federatedUser into the room - await matrix.getClient().invite(roomId, federatedUser.mxId); - - // federatedUser joins the room - await federatedUser.matrix.getClient().joinRoom(roomId); - - // Verify the federated room has the same accessRules - const federatedAccessRules = await federatedUser.matrix.getAccessRulesEvent(roomId); - expect(federatedAccessRules).toEqual(accessRules); - - // User sends a message to the room - const messageEventId = await matrix - .getClient() - .sendTextMessage(roomId, 'Hello from original user'); - expect(messageEventId).toBeDefined(); - - //wait for message to be read in from federated user - await expect.poll(async () => { - const room = federatedUser.matrix.getClient().getRoom(roomId); - if (!room) return 0; - const allEvents = room.getLiveTimeline().getEvents(); - - // Filter for m.room.message events only - const messageEvents = allEvents.filter(event => { - return event.getType() === 'm.room.message'; - }); - console.log(`#all events received:${allEvents.length}, # m.room.message received: ${messageEvents.length}`); - return messageEvents.length; - }, { - message: 'Expected room to have events', - timeout: 10000, // 10 secondes total - intervals: [1000] // Vérifie toutes les 1000ms (1 seconde) - }).toBeGreaterThan(0); - - - federatedMas.deactivateUser(federatedUser.masId); - }); - - test.afterAll(async () => { - masAdmin.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/private-encrypted-room.spec.ts b/tests/integration/api/room-access-rules/private-encrypted-room.spec.ts deleted file mode 100644 index 279d9dd..0000000 --- a/tests/integration/api/room-access-rules/private-encrypted-room.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - addModeratorToRoom, - createPrivateEncryptedRoom, - expectErrorWhenSendStateEvent, - loginWithNewUser, - setDefaultPowerLevel, - standardUserOptions, -} from './room-utils'; -import { EventType, JoinRule } from 'matrix-js-sdk'; - -test.describe('API - Private Encrypted Room', () => { - let matrix: MatrixApi; - let masId: string; - let masAdminClient: MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - const userData = await loginWithNewUser(masAdminClient, standardUserOptions()); - masId = userData.masId; - matrix = userData.matrix; - }); - - test('Should create private encrypted room with correct properties', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('restricted'); - expect(accessRules.force_unencrypted_at_creation).toBe(false); - expect(accessRules.visibility).toBe('private'); - - expect(await matrix.isRoomEncrypted(roomId)).toBe(true); - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should modify joinRule between invite and public', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, EventType.RoomJoinRules, { join_rule: JoinRule.Public }); - - expect(await matrix.getJoinRule(roomId)).toBe('public'); - - await matrix.sendStateEvent(roomId, EventType.RoomJoinRules, { join_rule: JoinRule.Invite }); - - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should change access rules from restricted to unrestricted', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, AccessRulesEventType, { rule: 'unrestricted' }); - - expect((await matrix.getAccessRulesEvent(roomId)).rule).toBe('unrestricted'); - }); - - test('Should return 403 error when changing access rules back to restricted', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, AccessRulesEventType, { rule: 'unrestricted' }); - expect((await matrix.getAccessRulesEvent(roomId)).rule).toBe('unrestricted'); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted' }, - 403 - ); - }); - - test('Should return 403 error when non admin send rules from restricted to unrestricted', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - const mod = await addModeratorToRoom(matrix, roomId, masAdminClient); - - await expectErrorWhenSendStateEvent( - mod.matrix, - roomId, - AccessRulesEventType, - { rule: 'unrestricted' }, - 403 - ); - }); - - //TODO : fix this - test.skip('Should return 403 error when invite external with PL user defaults admin', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - //change users_default PL to 100 - await setDefaultPowerLevel(matrix, roomId, 100); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'unrestricted' }, - 403 - ); - }); - - test.afterAll(async () => { - await masAdminClient.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/private-unencrypted-room.spec.ts b/tests/integration/api/room-access-rules/private-unencrypted-room.spec.ts deleted file mode 100644 index 8fa81d7..0000000 --- a/tests/integration/api/room-access-rules/private-unencrypted-room.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - createPrivateUnencryptedRoom, - expectErrorWhenSendStateEvent, - loginWithNewUser, - standardUserOptions, -} from './room-utils'; -import { EventType, JoinRule } from 'matrix-js-sdk'; - -test.describe('API - Private Unencrypted Room', () => { - let matrix: MatrixApi; - let masId: string; - let masAdminClient: MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - const userData = await loginWithNewUser(masAdminClient, standardUserOptions()); - masId = userData.masId; - matrix = userData.matrix; - }); - - test('Should create private unencrypted room with correct properties', async () => { - const roomId = await createPrivateUnencryptedRoom(matrix); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('restricted'); - expect(accessRules.force_unencrypted_at_creation).toBe(true); - expect(accessRules.visibility).toBe('private'); - - expect(await matrix.isRoomEncrypted(roomId)).toBe(false); - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should modify joinRule between invite and public', async () => { - const roomId = await createPrivateUnencryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, EventType.RoomJoinRules, { join_rule: JoinRule.Public }); - - expect(await matrix.getJoinRule(roomId)).toBe('public'); - - await matrix.sendStateEvent(roomId, EventType.RoomJoinRules, { join_rule: JoinRule.Invite }); - - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should change access rules from restricted to unrestricted', async () => { - const roomId = await createPrivateUnencryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, AccessRulesEventType, { - rule: 'unrestricted', - force_unencrypted_at_creation: true, - }); - - expect((await matrix.getAccessRulesEvent(roomId)).rule).toBe('unrestricted'); - }); - - test('Should return 403 error when changing access rules back to restricted', async () => { - const roomId = await createPrivateUnencryptedRoom(matrix); - - await matrix.sendStateEvent(roomId, AccessRulesEventType, { - rule: 'unrestricted', - force_unencrypted_at_creation: true, - }); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted' }, - 403 - ); - }); - - test.afterAll(async () => { - await masAdminClient.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/public-room.spec.ts b/tests/integration/api/room-access-rules/public-room.spec.ts deleted file mode 100644 index c15db2b..0000000 --- a/tests/integration/api/room-access-rules/public-room.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - createPublicRoom, - expectErrorWhenSendStateEvent, - loginWithNewUser, - standardUserOptions, -} from './room-utils'; -import { EventType } from 'matrix-js-sdk'; - -test.describe('API - Public Room', () => { - let masId: string; - let matrix: MatrixApi; - let masAdminClient: MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - const userData = await loginWithNewUser(masAdminClient, standardUserOptions()); - masId = userData.masId; - matrix = userData.matrix; - }); - - test('Should create public room with correct properties', async () => { - const roomId = await createPublicRoom(matrix); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('restricted'); - expect(accessRules.force_unencrypted_at_creation).toBe(false); - expect(accessRules.visibility).toBe('public'); - expect(await matrix.isRoomEncrypted(roomId)).toBe(false); - expect(await matrix.getJoinRule(roomId)).toBe('public'); - }); - - test('Should return 403 error when changing access rules to unrestricted', async () => { - const roomId = await createPublicRoom(matrix); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'unrestricted' }, - 403 - ); - }); - - test('Should return 403 error when changing encryption', async () => { - const roomId = await createPublicRoom(matrix); - - await expectErrorWhenSendStateEvent(matrix, roomId, EventType.RoomEncryption, {}, 403); - }); - - test('Should create room with public preset correctly sets the visibility to public', async () => { - const roomId = await matrix.createRoom({ - name: "Public Room", - joinRule: 'public', - preset: 'public_chat', - }); - expect(roomId).toBeDefined(); - - const accessRulesEvent = await matrix.getAccessRulesEvent(roomId); - expect(accessRulesEvent).toBeDefined(); - expect(accessRulesEvent.visibility).toBe('public'); - }); - - test('Should create public room with default retention of 3 months', async () => { - const roomId = await createPublicRoom(matrix); - expect(roomId).toBeDefined(); - - const roomRetention = await matrix.getRoomRetentionEvent(roomId) - expect(roomRetention).toBeDefined(); - expect(roomRetention.max_lifetime).toBe(3 * 30 * 24 * 60 * 60 * 1000); - }); - - test('Should fail to create public room with custom retention superior to 3 months', async () => { - await expect(createPublicRoom(matrix, 'Public Room', 4 * 30 * 24 * 60 * 60 * 1000)).rejects.toThrow() - }); - - test('Should fail to set retention to a value superior to 3 months or to unset it', async () => { - const roomId = await createPublicRoom(matrix); - expect(roomId).toBeDefined(); - - await expect(matrix.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 4 * 30 * 24 * 60 * 60 * 1000 })) - .rejects - .toThrow(); - - await expect(matrix.sendStateEvent(roomId, "m.room.retention", { })) - .rejects - .toThrow(); - }); - - test('Should allow to set retention to a value inferior to 3 months', async () => { - const roomId = await createPublicRoom(matrix); - expect(roomId).toBeDefined(); - - await expect(matrix.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 2 * 30 * 24 * 60 * 60 * 1000 })) - .resolves - .toBeDefined(); - }); - - test.afterAll(async () => { - masAdminClient.deactivateUser(masId); - }); -}); diff --git a/tests/integration/api/room-access-rules/room-utils.ts b/tests/integration/api/room-access-rules/room-utils.ts deleted file mode 100644 index 1278eeb..0000000 --- a/tests/integration/api/room-access-rules/room-utils.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { expect } from '@playwright/test'; -import { MatrixApi } from '../../../../utils/matrix-api'; -import { - MATRIX_URL, - EXTERNAL_MATRIX_URL, - EXTERNAL_MAS_URL, - INVITED_EMAIL_DOMAIN, - MAS_URL, - OTHER_MATRIX_URL, - OTHER_EMAIL_DOMAIN, - OTHER_MAS_URL, - STANDARD_EMAIL_DOMAIN, - TEST_USER_PASSWORD, - TEST_USER_PREFIX, -} from '../../../../utils/config'; -import type { MasAdminClient } from '../../../../utils/mas-admin'; -import { EventType } from 'matrix-js-sdk'; -import { Credentials } from '../../../../utils/auth-helpers'; - -/** - * Options for loginWithNewUser function - */ -interface CreateUserOptions { - domain: string; - username_prefix: string; - password: string; - matrix_url: string; - mas_url: string; - displayName?: string; -} - -export function standardUserOptions(overrides: Partial = {}): CreateUserOptions { - return { - domain: STANDARD_EMAIL_DOMAIN, - password: TEST_USER_PASSWORD, - matrix_url: MATRIX_URL, - mas_url: MAS_URL, - displayName: undefined, - username_prefix: TEST_USER_PREFIX, - ...overrides, - }; -} -export function externalUserOptions(overrides: Partial = {}): CreateUserOptions { - return { - domain: INVITED_EMAIL_DOMAIN, - password: TEST_USER_PASSWORD, - matrix_url: EXTERNAL_MATRIX_URL, - mas_url: EXTERNAL_MAS_URL, - displayName: undefined, - username_prefix: TEST_USER_PREFIX, - ...overrides, - }; -} - -export function federatedUserOptions( - overrides: Partial = {} -): CreateUserOptions { - return { - domain: OTHER_EMAIL_DOMAIN, - password: TEST_USER_PASSWORD, - matrix_url: OTHER_MATRIX_URL, - mas_url: OTHER_MAS_URL, - displayName: undefined, - username_prefix: TEST_USER_PREFIX, - ...overrides, - }; -} -/** - * Helper function to login with a new user - * Creates a new MAS user and returns the userId, username, and authenticated MatrixApi instance - */ -export async function loginWithNewUser( - masAdminClient: MasAdminClient, - opts: CreateUserOptions -): Promise<{ - mxId: string; - username: string; - matrix: MatrixApi; - masId: string; - credentials: Credentials; -}> { - const username = `${opts.username_prefix}${Date.now()}`; - - const masId = await masAdminClient.createUserWithPassword( - username, - `${username}@${opts.domain}`, - opts.password, - opts.displayName - ); - - const matrix = new MatrixApi(opts.matrix_url, opts.mas_url); - const credentials = await matrix.login(username, opts.password); - - return { mxId:credentials.userId, username, matrix, masId, credentials }; -} - -/** - * Helper function to test that sending a state event returns an error with the expected status code - */ -export async function expectErrorWhenSendStateEvent( - matrix: MatrixApi, - roomId: string, - eventType: string, - content: Record, - expectedStatus: number = 403 -): Promise { - await expect(matrix.sendStateEvent(roomId, eventType, content)).rejects.toMatchObject({ - httpStatus: expectedStatus, - }); -} - -export async function createPrivateEncryptedRoom( - matrix: MatrixApi, - name: string = 'Private Room' -): Promise { - return matrix.createRoom({ - name, - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - accessRules: { - rule: 'restricted', - force_unencrypted_at_creation: false, - visibility: 'private', - }, - }); -} - -export async function createPrivateUnencryptedRoom( - matrix: MatrixApi, - name: string = 'Private Unencrypted Room' -): Promise { - return matrix.createRoom({ - name, - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - encryption: false, - accessRules: { - rule: 'restricted', - force_unencrypted_at_creation: true, - visibility: 'private', - }, - }); -} - -export async function createPublicRoom( - matrix: MatrixApi, - name: string = 'Public Room', - retention_max_lifetime?: number, -): Promise { - return matrix.createRoom({ - name, - joinRule: 'public', - preset: 'public_chat', - visibility: 'public', - accessRules: { - rule: 'restricted', - force_unencrypted_at_creation: false, - visibility: 'public', - }, - power_level_content_override: { - events: { - 'm.room.name': 50, - 'm.room.avatar': 50, - 'm.room.power_levels': 100, - 'm.room.history_visibility': 100, - 'm.room.canonical_alias': 50, - 'm.room.tombstone': 100, - 'm.room.server_acl': 100, - 'm.room.encryption': 100, - 'org.matrix.msc3401.call.member': 0, - }, - }, - retention_max_lifetime: retention_max_lifetime, - }); -} - -/** - * Sets the power level for a specific user in a room by updating the existing power levels event. - * This function first fetches the current power levels, then updates the specified user's PL. - * - * @param matrix - The Matrix API instance - * @param roomId - The room ID - * @param userId - The user ID to set power level for - * @param powerLevel - The power level to set (e.g., 50 for moderator, 100 for admin) - */ -export async function setUserPowerLevel( - matrix: MatrixApi, - roomId: string, - userId: string, - powerLevel: number -): Promise { - // Get current power levels - const currentPowerLevels = await matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - - // Create updated power levels content - const updatedPowerLevels = { - ...currentPowerLevels, - users: { - ...currentPowerLevels.users, - [userId]: powerLevel, - }, - }; - - // Send the updated power levels event - await matrix - .getClient() - .sendStateEvent(roomId, EventType.RoomPowerLevels, updatedPowerLevels, ''); -} - -/** - * Sets the power level for users_default - * - * @param matrix - The Matrix API instance - * @param roomId - The room ID - * @param powerLevel - The power level to set (e.g., 50 for moderator, 100 for admin) - */ -export async function setDefaultPowerLevel( - matrix: MatrixApi, - roomId: string, - defaultPowerLevel: number -): Promise { - // Get current power levels - const currentPowerLevels = await matrix - .getClient() - .getStateEvent(roomId, EventType.RoomPowerLevels, ''); - - const updatedPowerLevels = { - ...currentPowerLevels, - users_default: defaultPowerLevel, - }; - - await matrix - .getClient() - .sendStateEvent(roomId, EventType.RoomPowerLevels, updatedPowerLevels, ''); -} - -/** - * Creates a new user and adds them to a room with default power level. - * - * @param matrix - The Matrix API instance of the room admin/creator - * @param roomId - The room ID to add the user to - * @returns Promise resolving to the created user object (mxId, matrix, masId) - */ -export async function addNewUserToRoom( - matrix: MatrixApi, - roomId: string, - masAdminClient: MasAdminClient, - createUserOptions: CreateUserOptions -): Promise<{ mxId: string; matrix: MatrixApi; masId: string }> { - // Create new user - const user = await loginWithNewUser(masAdminClient, createUserOptions); - - // Invite user to room - await matrix.getClient().invite(roomId, user.mxId); - - // User joins room - await user.matrix.getClient().joinRoom(roomId); - - return user; -} - -/** - * Creates a new user and adds them to a room as a moderator (PL=50). - * - * @param matrix - The Matrix API instance of the room admin/creator - * @param roomId - The room ID to add the moderator to - * @returns Promise resolving to the created moderator user object (mxId, matrix, masId) - */ -export async function addModeratorToRoom( - matrix: MatrixApi, - roomId: string, - masAdminClient: MasAdminClient -): Promise<{ mxId: string; matrix: MatrixApi; masId: string }> { - // Create and add user to room - const user = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - - // Set user's power level to 50 (moderator) - await setUserPowerLevel(matrix, roomId, user.mxId, 50); - - return user; -} - -/** - * Creates another admin to a room as a admin (PL=100). - * - * @param matrix - The Matrix API instance of the room admin/creator - * @param roomId - The room ID to add the moderator to - * @returns Promise resolving to the created admin user object (mxId, matrix, masId) - */ -export async function addAdminToRoom( - matrix: MatrixApi, - roomId: string, - masAdminClient: MasAdminClient -): Promise<{ mxId: string; matrix: MatrixApi; masId: string }> { - // Create and add user to room - const user = await addNewUserToRoom(matrix, roomId, masAdminClient, standardUserOptions()); - - // Set user's power level to 100 (admin) - await setUserPowerLevel(matrix, roomId, user.mxId, 100); - - return user; -} diff --git a/tests/integration/api/room-access-rules/room.spec.ts b/tests/integration/api/room-access-rules/room.spec.ts deleted file mode 100644 index 1a8a8f3..0000000 --- a/tests/integration/api/room-access-rules/room.spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { AccessRulesEventType, type MatrixApi } from '../../../../utils/matrix-api'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { - createPrivateEncryptedRoom, - createPublicRoom, - expectErrorWhenSendStateEvent, - loginWithNewUser, - standardUserOptions, -} from './room-utils'; - -test.describe('API - Room', () => { - let matrix: MatrixApi; - let masId: string; - let masAdminClient: MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - const userData = await loginWithNewUser(masAdminClient, standardUserOptions()); - masId = userData.masId; - matrix = userData.matrix; - }); - - test('Should create private room without access rules', async () => { - const roomId = await matrix.createRoom({ - name: 'name', - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - }); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('restricted'); - expect(accessRules.visibility).toBe('private'); - expect(accessRules.force_unencrypted_at_creation).toBe(undefined); - expect(await matrix.isRoomEncrypted(roomId)).toBe(true); - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should create public room without access rules', async () => { - const roomId = await matrix.createRoom({ - name: 'name', - joinRule: 'public', - preset: 'public_chat', - visibility: 'public', - }); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('restricted'); - expect(accessRules.visibility).toBe('public'); - expect(accessRules.force_unencrypted_at_creation).toBe(undefined); - expect(await matrix.isRoomEncrypted(roomId)).toBe(false); - expect(await matrix.getJoinRule(roomId)).toBe('public'); - }); - - test('Should create direct room without access rules', async () => { - const roomId = await matrix.createRoom({ - name: 'name', - joinRule: 'invite', - preset: 'trusted_private_chat', - visibility: 'private', - is_direct: true, - }); - expect(roomId).toBeDefined(); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules).toBeDefined(); - expect(accessRules.rule).toBe('direct'); - expect(accessRules.visibility).toBe('private'); - expect(accessRules.force_unencrypted_at_creation).toBe(undefined); - expect(await matrix.isRoomEncrypted(roomId)).toBe(true); - expect(await matrix.getJoinRule(roomId)).toBe('invite'); - }); - - test('Should return 403 error when updating visibility from private to public', async () => { - const roomId = await createPrivateEncryptedRoom(matrix); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules.visibility).toBe('private'); - expect(accessRules.rule).toBe('restricted'); - - //update visibility from private to public is forbidden - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted', visibility: 'public' }, - 403 - ); - }); - - test('Should return 403 error when removing "public" visibility', async () => { - const roomId = await createPublicRoom(matrix); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules.visibility).toBe('public'); - expect(accessRules.rule).toBe('restricted'); - - //set a visibility when undefined should not be possible - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted', visibility: undefined }, - 403 - ); - }); - - test('Should return 403 error when changing force_unencrypted_at_creation', async () => { - const roomId = await matrix.createRoom({ - name: 'name', - joinRule: 'invite', - preset: 'trusted_private_chat', - is_direct: true, - }); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules.force_unencrypted_at_creation).toBe(undefined); - - await expectErrorWhenSendStateEvent( - matrix, - roomId, - AccessRulesEventType, - { rule: 'restricted', force_unencrypted_at_creation: true }, - 403 - ); - }); - - test('Should keep access rules when upgrading room from v1 to V9', async () => { - const roomId = await matrix.createRoom({ - name: 'name', - joinRule: 'invite', - preset: 'private_chat', - visibility: 'private', - encryption: false, - accessRules: { - rule: 'unrestricted', - force_unencrypted_at_creation: true, - visibility: 'private', - }, - room_version: '1', - }); - - const accessRules = await matrix.getAccessRulesEvent(roomId); - expect(accessRules.rule).toBe('unrestricted'); - expect(accessRules.force_unencrypted_at_creation).toBe(true); - expect(accessRules.visibility).toBe('private'); - - const upgradeResponse = await matrix.upgradeRoom(roomId, '9'); - - //check that access rule exists in the upgraded room with correct value - const replacementRoomId = upgradeResponse.replacement_room; - console.log('Ugraded room id', replacementRoomId); - const upgradedAccessRules = await matrix.getAccessRulesEvent(replacementRoomId); - expect(upgradedAccessRules.rule).toBe('unrestricted'); - expect(upgradedAccessRules.force_unencrypted_at_creation).toBe(true); - expect(upgradedAccessRules.visibility).toBe('private'); - }); - - test.afterAll(async () => { - masAdminClient.deactivateUser(masId); - }); -}); diff --git a/tests/integration/web/minimal/minimal-scenario.spec.ts b/tests/minimal/minimal-scenario.spec.ts similarity index 65% rename from tests/integration/web/minimal/minimal-scenario.spec.ts rename to tests/minimal/minimal-scenario.spec.ts index a1e0e96..d69ff00 100644 --- a/tests/integration/web/minimal/minimal-scenario.spec.ts +++ b/tests/minimal/minimal-scenario.spec.ts @@ -1,23 +1,14 @@ -import { test, expect } from '../../../../fixtures/auth-fixture'; +import { test, expect } from '../../fixtures/auth-fixture'; import { - createMasTestUser, generateRoomName, generateTestUserData, - loginWithPassword, openResetPasswordEmail, - TestUser, -} from '../../../../utils/auth-helpers'; -import { - ELEMENT_URL, - INVITED_EMAIL_DOMAIN, - OTHER_EMAIL_DOMAIN, - STANDARD_EMAIL_DOMAIN, -} from '../../../../utils/config'; -import { getLatestVerificationCode, waitForMessage } from '../../../../utils/mailpit'; -import { TchapAppPage } from '../../../../utils/TchapAppPage'; +} from '../../utils/auth-helpers'; +import { ELEMENT_URL, INVITED_EMAIL_DOMAIN, STANDARD_EMAIL_DOMAIN } from '../../utils/config'; +import { getLatestVerificationCode, waitForMessage } from '../../utils/mailpit'; +import { TchapAppPage } from '../../utils/TchapAppPage'; import path from 'node:path'; import type { Page } from '@playwright/test'; -import { MasAdminClient } from '../../../../utils/mas-admin'; //this scenario is one big test to cover all the scenario on a not MAS synapse (dev02 - a) and one MAS synapse (ext01 - e) @@ -90,12 +81,25 @@ async function createUnencryptedPrivateRoom(page: Page, roomName: string): Promi return roomName; } -//TODO : break down test suite in atomic tests +// Helper function to create an external private room +async function createExternalPrivateRoom( + page: Page, + roomName: string = 'Salon ouvert aux externes' +): Promise { + const appPage = new TchapAppPage(page); + await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); + await page.getByText('Nouveau salon').click(); + await page.getByLabel('Créer un salon').click(); + await appPage.selectRoomType('Salon privé sécurisé avec externes'); + await page.getByRole('textbox', { name: 'Nom' }).fill(roomName); + await page.getByRole('button', { name: 'Créer un nouveau salon' }).click(); + return roomName; +} test.describe .serial('Minimal scenario', () => { - test.setTimeout(240_000); + test.setTimeout(120_000); const external_user = generateTestUserData(INVITED_EMAIL_DOMAIN); const agent_user = generateTestUserData(STANDARD_EMAIL_DOMAIN); @@ -103,41 +107,13 @@ test.describe const public_room_name = generateRoomName('public_room_name'); const private_crypted_room_name = generateRoomName('private_crypted_room_name'); const private_uncrypted_room_name = generateRoomName('private_uncrypted_room_name'); - - let invitee1, invitee1_search_name:string, invitee1_display_name:string; - let invitee2_email:string, invitee2_display_name:string; - let invitee3, invitee3_search_name:string, invitee3_display_name:string; - let masAdminClient:MasAdminClient ; - let federatedMasAdminClient:MasAdminClient; - - test.beforeAll(async () => { - masAdminClient = await MasAdminClient.createDefaultMAS(); - federatedMasAdminClient = await MasAdminClient.createFederatedMAS(); - - - // create invitee1 - invitee1 = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); - invitee1_search_name = invitee1.displayName.split(' ')[0].toLocaleLowerCase() + ' ' + invitee1.displayName.split(' ')[1].toLocaleLowerCase(); - invitee1_display_name = invitee1.displayName; - - // create invitee2 - invitee2_email = 'testeur@agent2.tchap.incubateur.net'; - invitee2_display_name = 'Testeur [Incubateur]'; - - // Créer invitee3 - invitee3 = await createMasTestUser(OTHER_EMAIL_DOMAIN, federatedMasAdminClient); - invitee3_search_name = invitee3.displayName.split(' ')[0].toLocaleLowerCase() + ' ' + invitee3.displayName.split(' ')[1].toLocaleLowerCase(); - invitee3_display_name = invitee3.displayName; - }); - - /* * tested: * creer compte agent * creer salon privé * creer salon privé non chiffré * creer salon public - * inviter par email, + * creer salon privé ouverts aux externes * inviter un externe * envoyer fichier, fichier vérolé * activer la sauvegarde @@ -148,50 +124,16 @@ test.describe * TODO : A. expirer le compte, vérifier que les clients affichent un truc cohérent, */ - test('test room creation', async ({ page, context, screenChecker, authenticatedUser }) => { - //creer salon public - await createPublicRoom(page, public_room_name); - - //chercher salon public - await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); - await page.getByRole('menuitem', { name: 'Rejoindre un salon public', exact: true }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(public_room_name); - await expect(page.getByLabel('Suggestions').getByText(public_room_name)).toBeVisible(); - await page.getByRole('textbox', { name: 'Rechercher' }).press('Escape'); - - //creer salon privé - await createEncryptedPrivateRoom(page, private_crypted_room_name); + test('internal user', async ({ page, context, screenChecker }) => { + const invitee1_search_name = 'olivier test1'; // TODO : ensure that invitee exists in the environment + const invitee1_display_name = 'Olivier Test1'; // TODO : ensure that invitee exists in the environment - //creer salon privé non chiffré - await createUnencryptedPrivateRoom(page, private_uncrypted_room_name); + const invitee2_email = 'testeur@agent2.tchap.incubateur.net'; // TODO : ensure that invitee exists in the environment + const invitee2_display_name = 'Testeur [Incubateur]'; // TODO : ensure that invitee exists in the environment - }) - - test('send files', async ({ page, context, screenChecker, authenticatedUser }) => { - //creer salon privé - await createEncryptedPrivateRoom(page, private_crypted_room_name); - - //envoyer fichier png - await page - .locator(".mx_MessageComposer_actions input[type='file']") - .setInputFiles(path.join(__dirname, '../../../../sample-files/element.png')); - - await page.getByRole('button', { name: 'Envoyer' }).click(); - await expect( page.getByRole('link', { name: 'element.png' })).toBeVisible(); - - - //envoyer fichier vérolé - await page - .locator(".mx_MessageComposer_actions input[type='file']") - .setInputFiles(path.join(__dirname, '../../../../sample-files/eicar.com')); - await page.getByRole('button', { name: 'Envoyer' }).click(); - await page.getByRole('listitem').filter({ hasText: /^Contenu bloqué$/ }); - - }) - - test('internal user setup account', async ({ page, context, screenChecker }) => { - + // Grant clipboard permissions to browser context + await context.grantPermissions(['clipboard-read', 'clipboard-write']); //create account await page.goto(`${ELEMENT_URL}/#/welcome`, { waitUntil: 'load' }); @@ -263,7 +205,7 @@ test.describe if (await page.getByRole('button', { name: 'Ignorer' }).isVisible()) { await page.getByRole('button', { name: 'Ignorer' }).click(); } - /* + //creer salon public await createPublicRoom(page, public_room_name); @@ -276,8 +218,7 @@ test.describe //creer salon privé await createEncryptedPrivateRoom(page, private_crypted_room_name); -*/ -/* + //creer salon privé non chiffré await createUnencryptedPrivateRoom(page, private_uncrypted_room_name); @@ -294,39 +235,47 @@ test.describe //inviter agents by email await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); await page.getByRole('textbox', { name: 'Rechercher' }).fill(invitee2_email); - await page.getByText(invitee2_display_name).first().click(); await page.getByRole('button', { name: 'Inviter' }).click(); await expect( page.getByTestId('virtuoso-item-list').getByText(invitee2_display_name) ).toBeVisible(); - //inviter agents from other homeserver by name - await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(invitee3_search_name); - await page.getByText(invitee3_display_name).first().click(); - await page.getByRole('button', { name: 'Inviter' }).click(); - await expect( - page.getByTestId('virtuoso-item-list').getByText(invitee3_display_name) - ).toBeVisible(); + //envoyer fichier png + await page + .locator(".mx_MessageComposer_actions input[type='file']") + .setInputFiles(path.join(__dirname, '../../sample-files/element.png')); - //creer salon privé - await createEncryptedPrivateRoom(page, private_crypted_room_name); + await page.getByRole('button', { name: 'Envoyer' }).click(); + await page.getByRole('link', { name: 'element.png' }).click(); + await page.getByRole('button', { name: 'Fermer' }).click(); + + //envoyer fichier vérolé + await page + .locator(".mx_MessageComposer_actions input[type='file']") + .setInputFiles(path.join(__dirname, '../../sample-files/eicar.com')); + await page.getByRole('button', { name: 'Envoyer' }).click(); + await page.getByRole('listitem').filter({ hasText: /^Contenu bloqué$/ }); + + //creer salon privé ouvert aux externes + await createExternalPrivateRoom(page); //inviter agent externe await page.getByRole('button', { name: 'Personnes' }).click(); await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); await page.getByRole('textbox', { name: 'Rechercher' }).fill(external_user.email); await page.getByRole('button', { name: 'Inviter' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); //await expect(page.getByTestId('virtuoso-item-list').getByText(external_user.username)).toBeVisible(); await expect( page.getByRole('option', { name: external_user.email.split('@')[0] }) ).toBeVisible(); //just the localpart of the emailawait page.getByRole('option', { name: 'user.local_1775742819448_6984' }).click(); -*/ + //disconnect await page.getByRole('button', { name: 'Avatar' }).click(); - await page.getByRole('button', { name: 'Supprimer cet appareil' }).click(); - + await page.getByRole('button', { name: 'Se déconnecter' }).click(); + await page + .locator('#mx_Dialog_Container') + .getByRole('button', { name: 'Se déconnecter' }) + .click(); //reset passsword await page.getByRole('link', { name: 'Se connecter' }).click(); await page.getByRole('textbox', { name: 'Votre adresse mail' }).fill(agent_user.email); @@ -356,93 +305,12 @@ test.describe ).toBeVisible(); }); - test('test invites', async ({ page, context, screenChecker }) => { - //we can't use authenticatedUser fixture as the identity server is not well setup in the profile - //this blocks invite by email - //inviter - let inviter:TestUser; - inviter = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); - - //login with inviter - await loginWithPassword(page, {email:inviter.email, password:inviter.password}, screenChecker); - - //creer salon privé non chiffré - await createUnencryptedPrivateRoom(page, private_uncrypted_room_name); - - //inviter agents by name - await page.getByRole('button', { name: 'Personnes' }).click(); - await page.getByRole('button', { name: 'Inviter', exact: true }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(invitee1_search_name); - await page.getByText(invitee1_display_name).first().click(); - await page.getByRole('button', { name: 'Inviter' }).click(); - await expect( - page.getByTestId('virtuoso-item-list').getByText(invitee1_display_name) - ).toBeVisible(); - - //inviter agents by email - await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(invitee2_email); - await page.getByText(invitee2_display_name).first().click(); - await page.getByRole('button', { name: 'Inviter' }).click(); - await expect( - page.getByTestId('virtuoso-item-list').getByText(invitee2_display_name) - ).toBeVisible(); - - //inviter agents from other homeserver by name - await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(invitee3_search_name); - await page.getByText(invitee3_display_name).first().click(); - await page.getByRole('button', { name: 'Inviter' }).click(); - await expect( - page.getByTestId('virtuoso-item-list').getByText(invitee3_display_name) - ).toBeVisible(); - - //creer salon privé - await createEncryptedPrivateRoom(page, private_crypted_room_name); - - //inviter agent externe - await page.getByRole('button', { name: 'Personnes' }).click(); - await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(external_user.email); - await page.getByRole('button', { name: 'Inviter' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); - //await expect(page.getByTestId('virtuoso-item-list').getByText(external_user.username)).toBeVisible(); - await expect( - page.getByRole('option', { name: external_user.email.split('@')[0] }) - ).toBeVisible(); //just the localpart of the email - - }) - - - test('external user', async ({page, screenChecker, browser }) => { - //we can't use authenticatedUser fixture as the identity server is not well setup in the profile - //this blocks invite by email - let inviter:TestUser; - inviter = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); - - //login with inviter - await loginWithPassword(page, {email:inviter.email, password:inviter.password}, screenChecker); - - //creer salon privé - await createEncryptedPrivateRoom(page, private_crypted_room_name); - - //inviter agent externe - await page.getByRole('button', { name: 'Personnes' }).click(); - await page.getByRole('button', { name: 'Inviter dans ce salon' }).click(); - await page.getByRole('textbox', { name: 'Rechercher' }).fill(external_user.email); - await page.getByRole('button', { name: 'Inviter' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); - //await expect(page.getByTestId('virtuoso-item-list').getByText(external_user.username)).toBeVisible(); - await expect( - page.getByRole('option', { name: external_user.email.split('@')[0] }) - ).toBeVisible(); //just the localpart of the email - - + test('external user', async ({ screenChecker, browser }) => { const context_ext = await browser.newContext(); const page_ext = await context_ext.newPage(); //invitation takes time to be available - const { message, content } = await waitForMessage(external_user.email, 120000, 'Invitation'); + const { message, content } = await waitForMessage(external_user.email, 40000, 'Invitation'); console.log('Invitation received for user, but sydent is a bit slow, so wait 10 seconds'); await page_ext.waitForTimeout(10000); @@ -479,16 +347,16 @@ test.describe await page_ext.getByRole('button').filter({ hasText: 'Continuer' }).click(); await page_ext.getByRole('button').filter({ hasText: 'Continuer' }).click(); - await page_ext.waitForSelector('.mx_MatrixChat', { timeout: 60000 }); + await page_ext.waitForSelector('.mx_MatrixChat', { timeout: 20000 }); //rejoindre le salon await page_ext .locator('div') - .filter({ hasText: /^Salon vide$/ }) + .filter({ hasText: /^Salon ouvert aux externes$/ }) .first() .click(); await page_ext.getByRole('button', { name: 'Accepter' }).click(); - await expect(await page_ext.getByText('Chiffrement activé')).toBeVisible(); + await expect(await page_ext.getByText('a créé ce salon. C’est le début de')).toBeVisible(); //ne peut pas créer de salon await page_ext.getByRole('button', { name: 'Ajouter', exact: true }).click(); diff --git a/tests/auth/deactivated/tchap-deactivated-account.spec.ts b/tests/synapse/email-account-validity/tchap-deactivated-account.spec.ts similarity index 87% rename from tests/auth/deactivated/tchap-deactivated-account.spec.ts rename to tests/synapse/email-account-validity/tchap-deactivated-account.spec.ts index cd07ff6..ecbf475 100644 --- a/tests/auth/deactivated/tchap-deactivated-account.spec.ts +++ b/tests/synapse/email-account-validity/tchap-deactivated-account.spec.ts @@ -1,5 +1,14 @@ import { test, expect } from '../../../fixtures/auth-fixture'; -import { MasAdminClient } from '../../../utils/mas-admin'; +import { + addUserEmail, + createMasUserWithPassword, + deactivateMasUser, + deleteOauthLink, + getMasUserByEmail, + getOauthLinkBySubject, + oauthLinkExistsBySubject, + reactivateMasUser, +} from '../../../utils/mas-admin'; import { SCREENSHOTS_DIR, ELEMENT_URL } from '../../../utils/config'; import { performOidcLogin } from '../../../utils/auth-helpers'; import { getLatestVerificationCode } from '../../../utils/mailpit'; @@ -11,9 +20,8 @@ test.describe('Tchap : Login password', () => { userData, screenChecker, }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); //create user - userData.masId = await masAdminClient.createUserWithPassword( + userData.masId = await createMasUserWithPassword( userData.username, userData.email, userData.password @@ -32,7 +40,7 @@ test.describe('Tchap : Login password', () => { await page.locator('button[type="submit"]').click(); //deactivate user - await masAdminClient.deactivateUser(userData.masId); + await deactivateMasUser(userData.masId); //login with another browser page = await (await browser.newContext()).newPage(); @@ -58,17 +66,15 @@ test.describe('Tchap : Login password', () => { screenChecker, startTchapRegisterWithEmail, }) => { - const masAdminClient = await MasAdminClient.createDefaultMAS(); - //create user - userData.masId = await masAdminClient.createUserWithPassword( + userData.masId = await createMasUserWithPassword( userData.username, userData.email, userData.password ); //deactivate user - await masAdminClient.deactivateUser(userData.masId); + await deactivateMasUser(userData.masId); //register await startTchapRegisterWithEmail(page, userData.email); @@ -90,7 +96,7 @@ test.describe('Tchap : Login password', () => { await page.waitForSelector('.mx_MatrixChat', { timeout: 20000 }); //same user, but reactivated - const created_user = await masAdminClient.getUserByEmail(userData.email); + const created_user = await getMasUserByEmail(userData.email); console.log(created_user); expect(created_user.id).toBe(userData.masId); expect(created_user.attributes.deactivated_at).toBeNull(); @@ -101,16 +107,15 @@ test.describe('Tchap : Login password', () => { oidcUser, }) => { const screenshot_path = test.info().title.replace(' ', '_'); - const masAdminClient = await MasAdminClient.createDefaultMAS(); // Create a user in MAS with the same email as the Keycloak user console.log(`Creating MAS user with same email as Keycloak user: ${oidcUser.email}`); - const formerTchapAccountMasId = await masAdminClient.createUserWithPassword( + const formerTchapAccountMasId = await createMasUserWithPassword( oidcUser.username, oidcUser.email, oidcUser.password ); - await masAdminClient.deactivateUser(formerTchapAccountMasId); + await deactivateMasUser(formerTchapAccountMasId); const newTchapAccountWithIndex = { username: `${oidcUser.username}2`, @@ -119,7 +124,7 @@ test.describe('Tchap : Login password', () => { masId: '', }; //create another user with same email but different username - newTchapAccountWithIndex.masId = await masAdminClient.createUserWithPassword( + newTchapAccountWithIndex.masId = await createMasUserWithPassword( newTchapAccountWithIndex.username, newTchapAccountWithIndex.email, newTchapAccountWithIndex.password @@ -139,18 +144,18 @@ test.describe('Tchap : Login password', () => { }); // Verify the user in MAS is linked to the indexed account - const userAfterLogin = await masAdminClient.getUserByEmail(newTchapAccountWithIndex.email); + const userAfterLogin = await getMasUserByEmail(newTchapAccountWithIndex.email); expect(userAfterLogin.id).toBe(newTchapAccountWithIndex.masId); expect(userAfterLogin.attributes.username).toBe(newTchapAccountWithIndex.username); await expect(page.locator(`text=${newTchapAccountWithIndex.username}`)).toBeVisible(); - expect(await masAdminClient.oauthLinkExistsBySubject(oidcUser.username)).toBe(true); + expect(await oauthLinkExistsBySubject(oidcUser.username)).toBe(true); console.log( `Successfully verified account linking for user with email: ${newTchapAccountWithIndex.email}` ); } finally { // Clean up the MAS user - await masAdminClient.deactivateUser(newTchapAccountWithIndex.masId); + await deactivateMasUser(newTchapAccountWithIndex.masId); console.log(`Cleaned up MAS user: ${newTchapAccountWithIndex.username}`); } }); @@ -162,12 +167,11 @@ test.describe('Tchap : Login password', () => { screenChecker, }) => { test.setTimeout(30000); - const masAdminClient = await MasAdminClient.createDefaultMAS(); const screenshot_path = test.info().title.replace(' ', '_'); //create user - oidcUser.masId = await masAdminClient.createUserWithPassword( + oidcUser.masId = await createMasUserWithPassword( oidcUser.username, oidcUser.email, oidcUser.password @@ -177,10 +181,10 @@ test.describe('Tchap : Login password', () => { await performOidcLogin(page, oidcUser, screenshot_path); //deactivate, email is unset - await masAdminClient.deactivateUser(oidcUser.masId); + await deactivateMasUser(oidcUser.masId); try { //user has no email when deactivated - await masAdminClient.getUserByEmail(oidcUser.email); + await getMasUserByEmail(oidcUser.email); } catch (e) { expect(e).toBeDefined(); } @@ -194,14 +198,13 @@ test.describe('Tchap : Login password', () => { await expect(page.locator('text=Connecté')).toBeVisible(); //mas user is reactivated and email is set - const created_user = await masAdminClient.getUserByEmail(oidcUser.email); + const created_user = await getMasUserByEmail(oidcUser.email); expect(created_user.id).toBe(oidcUser.masId); expect(created_user.attributes.deactivated_at).toBeNull(); }); //legacy //skip it - /* test.skip('match account by email when account was deactivated but is reactivated by support', async ({ browser, page, @@ -293,5 +296,4 @@ test.describe('Tchap : Login password', () => { console.log(`Cleaned up MAS user: ${oidcUser.username}`); } }); - */ }); diff --git a/tests/integration/web/email-account-validity/tchap-expiration.spec.ts b/tests/synapse/email-account-validity/tchap-expiration.spec.ts similarity index 83% rename from tests/integration/web/email-account-validity/tchap-expiration.spec.ts rename to tests/synapse/email-account-validity/tchap-expiration.spec.ts index 271be61..83b7c41 100644 --- a/tests/integration/web/email-account-validity/tchap-expiration.spec.ts +++ b/tests/synapse/email-account-validity/tchap-expiration.spec.ts @@ -1,12 +1,12 @@ -import { test, expect } from '../../../../fixtures/auth-fixture'; +import { test, expect } from '../../../fixtures/auth-fixture'; import { createMasTestUser, loginWithPassword, openRenewAccountEmail, -} from '../../../../utils/auth-helpers'; -import { MasAdminClient } from '../../../../utils/mas-admin'; -import { STANDARD_EMAIL_DOMAIN } from '../../../../utils/config'; -import { setAccountExpiration } from '../../../../utils/synapse-admin'; +} from '../../../utils/auth-helpers'; +import { getMasUserByEmail } from '../../../utils/mas-admin'; +import { STANDARD_EMAIL_DOMAIN } from '../../../utils/config'; +import { setAccountExpiration } from '../../../utils/synapse-admin'; test.describe('Tchap : account expiration', () => { test('should show expiration message when account is expired', async ({ @@ -15,8 +15,7 @@ test.describe('Tchap : account expiration', () => { screenChecker: screen, }) => { // Create a new user with password authentication - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); + const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN); console.log(`Created test user: ${user.username} with email: ${user.email}`); // First, log in normally to verify the account works @@ -27,7 +26,7 @@ test.describe('Tchap : account expiration', () => { console.log('User successfully logged in'); // Get the user's Matrix ID - const masUser = await masAdminClient.getUserByEmail(user.email); + const masUser = await getMasUserByEmail(user.email); const matrixId = `@${masUser.attributes.username}:dev01.tchap.incubateur.net`; console.log(`User Matrix ID: ${matrixId}`); @@ -55,8 +54,6 @@ test.describe('Tchap : account expiration', () => { // Clean up await page.close(); - - await masAdminClient.deactivateUser(user.masId); }); test('should show expiration message inside app when account is expired', async ({ @@ -68,8 +65,7 @@ test.describe('Tchap : account expiration', () => { test.setTimeout(180_000); // Create a new user with password authentication - const masAdminClient = await MasAdminClient.createDefaultMAS(); - const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN, masAdminClient); + const user = await createMasTestUser(STANDARD_EMAIL_DOMAIN); console.log(`Created test user: ${user.username} with email: ${user.email}`); // First, log in normally to verify the account works @@ -80,7 +76,7 @@ test.describe('Tchap : account expiration', () => { console.log('User successfully logged in'); // Get the user's Matrix ID - const masUser = await masAdminClient.getUserByEmail(user.email); + const masUser = await getMasUserByEmail(user.email); const matrixId = `@${masUser.attributes.username}:dev01.tchap.incubateur.net`; console.log(`User Matrix ID: ${matrixId}`); @@ -117,7 +113,5 @@ test.describe('Tchap : account expiration', () => { await page.waitForSelector('.mx_MatrixChat', { timeout: 20000 }); await expect(page.getByRole('heading', { name: 'Votre compte Tchap a été' })).not.toBeVisible(); - - await masAdminClient.deactivateUser(user.masId); }); }); diff --git a/utils/TchapAppPage.ts b/utils/TchapAppPage.ts index a46ab03..ffafb56 100644 --- a/utils/TchapAppPage.ts +++ b/utils/TchapAppPage.ts @@ -193,12 +193,12 @@ export class TchapAppPage { * Select a room type in the Tchap create room dialog. * This function uses a robust selector to avoid flaky tests when selecting * between similar room type options (e.g., "Salon privé" vs "Salon privé sécurisé") - * + * * @param roomType The exact room type to select */ public async selectRoomType( roomType: - | 'Salon privé' + 'Salon privé' | 'Salon privé sécurisé' | 'Salon privé sécurisé avec externes' | 'Salon public' diff --git a/utils/api.ts b/utils/api.ts index 162db79..e0d2599 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details. */ import type { APIRequestContext } from '@playwright/test'; -import { Credentials } from './auth-helpers'; export type Verb = 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -78,14 +77,30 @@ export class Api { } } +/** + * Credentials for a user. + */ +export interface Credentials { + /** The base URL of the homeserver's CS API. */ + homeserverBaseUrl: string; + + accessToken: string; + userId: string; + deviceId: string; + /** The domain part of the user's matrix ID. */ + homeServer: string; + + password: string | null; // null for password-less users + username: string; // the localpart of the userId +} /** * A client-server API for interacting with a Matrix homeserver. */ export class ClientServerApi extends Api { public constructor(baseUrl: string, request: APIRequestContext) { - super(`${baseUrl}`); + super(`${baseUrl}/_matrix/client`); this.setRequest(request); } diff --git a/utils/auth-helpers.ts b/utils/auth-helpers.ts index b155564..76b022f 100644 --- a/utils/auth-helpers.ts +++ b/utils/auth-helpers.ts @@ -1,6 +1,6 @@ import { type BrowserContext, expect, type Page } from '@playwright/test'; import { createKeycloakUser, deleteKeycloakUser } from './keycloak-admin'; -import type { MasAdminClient } from './mas-admin'; +import { waitForMasUser, createMasUserWithPassword, deactivateMasUser } from './mas-admin'; import { ELEMENT_URL, KEYCLOAK_URL, @@ -9,6 +9,7 @@ import { TEST_USER_PASSWORD, TEST_USER_PREFIX, } from './config'; +import type { Credentials } from './api'; import type { ScreenCheckerFixture } from '../fixtures/auth-fixture'; import { getExpirationAccountLink as getRenewAccountLink, @@ -23,7 +24,6 @@ export interface TestUser { password: string; keycloakId?: string; masId?: string; - displayName: string; domain: string; } @@ -176,47 +176,26 @@ export async function performOidcLoginFromTchap( /** * Verify that a user was created in MAS after OIDC authentication */ -export async function verifyUserInMas( - user: TestUser, - masAdminClient: MasAdminClient -): Promise { - const masUser = await masAdminClient.waitForUser(user.email); +export async function verifyUserInMas(user: TestUser): Promise { + const masUser = await waitForMasUser(user.email); user.masId = masUser.id; } /** * Create a test user directly in MAS with password */ -export async function createMasTestUser( - domain: string, - masAdminClient: MasAdminClient -): Promise { +export async function createMasTestUser(domain: string): Promise { const user = generateTestUserData(domain); - const masId = await masAdminClient.createUserWithPassword( - user.username, - user.email, - user.password, - user.displayName - ); - /* - const masId = await createMasUserWithPassword( - user.username, - user.email, - user.password, - user.displayName - );*/ + const masId = await createMasUserWithPassword(user.username, user.email, user.password); return { ...user, masId }; } /** * Clean up a MAS test user */ -export async function cleanupMasTestUser( - user: TestUser, - masAdminClient: MasAdminClient -): Promise { +export async function cleanupMasTestUser(user: TestUser): Promise { if (user.masId) { - await masAdminClient.deactivateUser(user.masId); + await deactivateMasUser(user.masId); } } @@ -364,18 +343,6 @@ export function generateTestUserData(domain: string): TestUser { const username = `${TEST_USER_PREFIX}_${timestamp}_${randomSuffix}`; const localpart = `${TEST_USER_PREFIX}_${timestamp}_${randomSuffix}-${domain}`; const email = `${username}@${domain}`; - const domainDisplayName = domain - .split('.') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(''); - const displayName = - username - .split('.') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') + - ' [' + - domainDisplayName + - ']'; console.log('Using email: ', email); @@ -384,13 +351,11 @@ export function generateTestUserData(domain: string): TestUser { email: email, password: TEST_USER_PASSWORD, domain: domain, - displayName: displayName, }; } // Taken from element-mmodules /** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */ -//TODO: Tchap : it misses identity server which blocks inviting by email export async function populateLocalStorageWithCredentials(page: Page, credentials: Credentials) { await page.addInitScript( ({ credentials }) => { @@ -415,22 +380,3 @@ export async function populateLocalStorageWithCredentials(page: Page, credential { credentials } ); } - - -/** - * Credentials for a user. - */ -export interface Credentials { - /** The base URL of the homeserver's CS API. */ - homeserverBaseUrl: string; - - accessToken: string; - userId: string; - deviceId: string; - - /** The domain part of the user's matrix ID. */ - homeServer: string; - - password: string | null; // null for password-less users - username: string; // the localpart of the userId -} \ No newline at end of file diff --git a/utils/config.ts b/utils/config.ts index 820aa21..039acf0 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -2,60 +2,41 @@ import dotenv from 'dotenv'; import path from 'node:path'; // Determine which environment to use -export const env = process.env.ENV || 'dev01'; +export const env = process.env.ENV || 'local'; console.log(`Loading environment configuration for: ${env}`); // Load environment variables from the appropriate .env file dotenv.config({ path: path.resolve(__dirname, `../.env.${env}`) }); -// Load environment variables from the appropriate .secrets file -dotenv.config({ path: path.resolve(__dirname, `../.secrets.${env}`) }); - // Load environment variables from .env file //dotenv.config(); -// MAS URLs +// URLs export const MAS_URL = process.env.MAS_URL || ''; -export const OTHER_MAS_URL = process.env.OTHER_MAS_URL || ''; -export const EXTERNAL_MAS_URL = process.env.EXTERNAL_MAS_URL || ''; - export const KEYCLOAK_URL = process.env.KEYCLOAK_URL || ''; export const ELEMENT_URL = process.env.ELEMENT_URL || ''; - -//SYNAPSE -export const MATRIX_URL = process.env.MATRIX_URL || ''; -export const OTHER_MATRIX_URL = process.env.OTHER_MATRIX_URL || ''; -export const EXTERNAL_MATRIX_URL = process.env.EXTERNAL_MATRIX_URL || ''; - +export const BASE_URL = process.env.BASE_URL || ''; export const MAIL_URL = process.env.MAIL_URL || ''; + export const MAILPIT_USER = process.env.MAILPIT_USER || ''; export const MAILPIT_PWD = process.env.MAILPIT_PWD || ''; // Keycloak Admin Credentials -export const KEYCLOAK_ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USERNAME ; -export const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD ; -export const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM; +export const KEYCLOAK_ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USERNAME || 'admin'; +export const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD || 'admin'; +export const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM || 'proconnect-mock'; // MAS Admin API Credentials -export const MAS_ADMIN_URL = process.env.MAS_ADMIN_URL || MAS_URL; -export const MAS_ADMIN_CLIENT_ID = process.env.MAS_ADMIN_CLIENT_ID || ''; -export const MAS_ADMIN_CLIENT_SECRET = process.env.MAS_ADMIN_CLIENT_SECRET || ''; - -export const OTHER_MAS_ADMIN_URL = process.env.OTHER_MAS_ADMIN_URL || OTHER_MAS_URL; -export const OTHER_MAS_ADMIN_CLIENT_ID = process.env.OTHER_MAS_ADMIN_CLIENT_ID || ''; -export const OTHER_MAS_ADMIN_SECRET = process.env.OTHER_MAS_ADMIN_SECRET || ''; - -export const EXTERNAL_MAS_ADMIN_URL = process.env.EXTERNAL_MAS_ADMIN_URL || EXTERNAL_MAS_URL; -export const EXTERNAL_MAS_ADMIN_CLIENT_ID = process.env.EXTERNAL_MAS_ADMIN_CLIENT_ID || ''; -export const EXTERNAL_MAS_ADMIN_SECRET = process.env.EXTERNAL_MAS_ADMIN_SECRET || ''; +export const MAS_ADMIN_CLIENT_ID = process.env.MAS_ADMIN_CLIENT_ID || '01J44RKQYM4G3TNVANTMTDYTX6'; +export const MAS_ADMIN_CLIENT_SECRET = + process.env.MAS_ADMIN_CLIENT_SECRET || 'phoo8ahneir3ohY2eigh4xuu6Oodaewi'; // Test User Credentials export const TEST_USER_PREFIX = process.env.TEST_USER_PREFIX || 'user.test'; -export const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || ''; +export const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || 'Test@123456'; // Email domains for test users export const STANDARD_EMAIL_DOMAIN = process.env.STANDARD_EMAIL_DOMAIN || ''; -export const OTHER_EMAIL_DOMAIN = process.env.OTHER_EMAIL_DOMAIN || ''; export const INVITED_EMAIL_DOMAIN = process.env.INVITED_EMAIL_DOMAIN || ''; export const NOT_INVITED_EMAIL_DOMAIN = process.env.NOT_INVITED_EMAIL_DOMAIN || ''; export const WRONG_SERVER_EMAIL_DOMAIN = process.env.WRONG_SERVER_EMAIL_DOMAIN || ''; @@ -69,4 +50,10 @@ export const BROWSER_LOCALE = process.env.BROWSER_LOCALE || 'fr-FR'; export const USE_MAS = process.env.USE_MAS === 'true' || false; +// TODO Move all below to env file +// Fixed tests data, that can be used across environement +export const FIX_USER_USERNAME = 'Michelle_test'; +export const FIX_USER_PASSWORD = 'Michelle1313!'; +export const FIX_USER_EMAIL = 'Michelle1313!'; + export const SYNAPSE_ADMIN_TOKEN = process.env.SYNAPSE_ADMIN_TOKEN || ''; diff --git a/utils/mailpit.ts b/utils/mailpit.ts index 891d9fa..1d2b9a1 100644 --- a/utils/mailpit.ts +++ b/utils/mailpit.ts @@ -31,7 +31,11 @@ export async function getMailpitClient() { */ export async function getLatestVerificationCode(toEmail: string): Promise { try { - const { message, content } = await waitForMessage(toEmail, 40000, 'Votre code est'); + const { message, content } = await waitForMessage( + toEmail, + 40000, + 'Votre code est' + ); console.log('[Mailpit] Email content preview:', content.substring(0, 200)); @@ -118,7 +122,7 @@ export async function waitForMessage( const startTime = Date.now(); let retryCount = 0; - let retryDelay = 1000; // 1 second delay + const retryDelay = 1000; // 1 second delay while (Date.now() - startTime < maxWaitTimeMs) { try { @@ -152,7 +156,6 @@ export async function waitForMessage( console.log( `[Mailpit] Waiting for emails found for ${toEmail} with subject ${subject} , retrying... (${retryCount})` ); - retryDelay += retryDelay; await new Promise((resolve) => setTimeout(resolve, retryDelay)); } catch (error) { // Error occurred, retry diff --git a/utils/mas-admin.ts b/utils/mas-admin.ts index b776b4a..3d41ec9 100644 --- a/utils/mas-admin.ts +++ b/utils/mas-admin.ts @@ -1,576 +1,509 @@ import { type APIRequestContext, request } from '@playwright/test'; -import { - EXTERNAL_MAS_ADMIN_CLIENT_ID, - EXTERNAL_MAS_ADMIN_SECRET, - EXTERNAL_MAS_ADMIN_URL, - MAS_ADMIN_CLIENT_ID, - MAS_ADMIN_CLIENT_SECRET, - MAS_ADMIN_URL, - OTHER_MAS_ADMIN_CLIENT_ID, - OTHER_MAS_ADMIN_SECRET, - OTHER_MAS_ADMIN_URL, -} from './config'; - -interface MasAdminCredentials { - clientId: string; - secret: string; -} - -// Create a reusable API request context cache -const apiContextCache = new Map(); -const masAdminCredentialsCache = new Map(); - -/** - * MasAdminClient - A class-based client for interacting with MAS Admin API - * This class handles authentication, token caching, and API requests automatically - */ -class MasAdminClient { - private readonly baseUrl: string; - private readonly clientId: string; - private readonly secret: string; - private apiContext!: APIRequestContext; - private token: string | null = null; - - public static async createDefaultMAS() { - return MasAdminClient.create(MAS_ADMIN_URL, MAS_ADMIN_CLIENT_ID, MAS_ADMIN_CLIENT_SECRET); - } +import { MAS_URL, MAS_ADMIN_CLIENT_ID, MAS_ADMIN_CLIENT_SECRET } from './config'; - public static async createFederatedMAS() { - return MasAdminClient.create( - OTHER_MAS_ADMIN_URL, - OTHER_MAS_ADMIN_CLIENT_ID, - OTHER_MAS_ADMIN_SECRET - ); - } - - public static async createExternalMAS() { - return MasAdminClient.create( - EXTERNAL_MAS_ADMIN_URL, - EXTERNAL_MAS_ADMIN_CLIENT_ID, - EXTERNAL_MAS_ADMIN_SECRET - ); - } +// Create a reusable API request context +let apiContext: APIRequestContext | null = null; - /** - * Factory method to create and initialize a MasAdminClient in one step - * @param baseUrl The base URL of the MAS server - * @param clientId The client ID for authentication (required) - * @param secret The client secret for authentication (required) - * @returns A fully initialized MasAdminClient instance - */ - public static async create( - baseUrl: string, - clientId: string, - secret: string - ): Promise { - const client = new MasAdminClient(baseUrl, clientId, secret); - await client.initialize(); - return client; - } - - /** - * Create a new MAS Admin client instance - * @param baseUrl The base URL of the MAS server - * @param clientId The client ID for authentication (required) - * @param secret The client secret for authentication (required) - */ - private constructor(baseUrl: string, clientId: string, secret: string) { - this.baseUrl = baseUrl; - this.clientId = clientId; - this.secret = secret; - } - - /** - * Initialize the client - must be called before using the client - */ - public async initialize(): Promise { - this.apiContext = await request.newContext({ - baseURL: this.baseUrl, +async function getApiContext(): Promise { + if (!apiContext) { + //console.log(`[MAS API] Creating new API context with baseURL: ${MAS_URL}`); + apiContext = await request.newContext({ + baseURL: MAS_URL, ignoreHTTPSErrors: true, }); } + return apiContext; +} - /** - * Dispose the client resources - */ - public async dispose(): Promise { - if (this.apiContext) { - await this.apiContext.dispose(); - } +/** + * Get an admin access token for MAS + */ +export async function getMasAdminToken(): Promise { + //console.log(`[MAS API] Requesting admin token with client ID: ${MAS_ADMIN_CLIENT_ID}`); + const apiRequestContext = await getApiContext(); + const authHeader = Buffer.from(`${MAS_ADMIN_CLIENT_ID}:${MAS_ADMIN_CLIENT_SECRET}`).toString( + 'base64' + ); + + const response = await apiRequestContext.post('/oauth2/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${authHeader}`, + }, + form: { + grant_type: 'client_credentials', + scope: 'urn:mas:admin', + }, + }); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to get admin token: ${response.status()} - ${errorText}`); + throw new Error(`Failed to get MAS admin token: ${response.status()} - ${errorText}`); } - /** - * obtaining a new token each call to avoid token expired in long tests - */ - private async getToken(): Promise { - - const authHeader = Buffer.from(`${this.clientId}:${this.secret}`).toString('base64'); + const data = (await response.json()) as { access_token: string }; + //console.log(`[MAS API] Successfully obtained admin token ${data.access_token}`); + return data.access_token; +} - const response = await this.apiContext.post('/oauth2/token', { +/** + * Get user details from MAS by email + */ +export async function getMasUserByEmail(email: string): Promise { + //console.log(`[MAS API] Getting user details for email: ${email}`); + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + // Step 1: Get user ID from email + const emailResponse = await apiRequestContext.get( + `/api/admin/v1/user-emails?filter[email]=${encodeURIComponent(email)}`, + { headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${authHeader}`, - }, - form: { - grant_type: 'client_credentials', - scope: 'urn:mas:admin', + Authorization: `Bearer ${token}`, }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to get admin token: ${response.status()} - ${errorText}`); - throw new Error(`Failed to get MAS admin token: ${response.status()} - ${errorText}`); } + ); - const data = (await response.json()) as { access_token: string }; - this.token = data.access_token; - console.log(`[MAS API] Successfully obtained admin token for ${this.baseUrl}`); - - return this.token; + if (!emailResponse.ok()) { + const errorText = await emailResponse.text(); + console.error(`[MAS API] Failed to get user email: ${emailResponse.status()} - ${errorText}`); + throw new Error(`Failed to get MAS user email: ${emailResponse.status()} - ${errorText}`); } - /** - * Get user details from MAS by email - */ - public async getUserByEmail(email: string): Promise { - console.log(`[MAS API] Getting user details for email: ${email} on ${this.baseUrl}`); - const token = await this.getToken(); - - // Step 1: Get user ID from email - const emailResponse = await this.apiContext.get( - `/api/admin/v1/user-emails?filter[email]=${encodeURIComponent(email)}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - if (!emailResponse.ok()) { - const errorText = await emailResponse.text(); - console.error(`[MAS API] Failed to get user email: ${emailResponse.status()} - ${errorText}`); - throw new Error(`Failed to get MAS user email: ${emailResponse.status()} - ${errorText}`); - } - - const emailResult = await emailResponse.json(); - if (emailResult.data.length === 0) { - console.log(`[MAS API] No user found with email: ${email}`); - throw new Error(`[MAS API] No user found with email: ${email}`); - } - - if (emailResult.data.length > 1) { - console.log(`[MAS API] Multiple users found with email: ${email}`); - throw new Error(`[MAS API] Multiple users found with email: ${email}`); - } + const emailResult = await emailResponse.json(); + if (emailResult.data.length === 0) { + console.log(`[MAS API] No user found with email: ${email}`); + throw new Error(`[MAS API] No user found with email: ${email}`); + } - // Extract user_id from the attributes - const userId = emailResult.data[0].attributes.user_id; - console.log(`[MAS API] Found user ID: ${userId} for email: ${email}`); + if (emailResult.data.length > 1) { + console.log(`[MAS API] Multiple users found with email: ${email}`); + throw new Error(`[MAS API] Multiple users found with email: ${email}`); + } - // Step 2: Get complete user details using the user ID - const userResponse = await this.apiContext.get(`/api/admin/v1/users/${userId}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + // Extract user_id from the attributes + const userId = emailResult.data[0].attributes.user_id; + //console.log(`[MAS API] Found user ID: ${userId} for email: ${email}`); + + // Step 2: Get complete user details using the user ID + const userResponse = await apiRequestContext.get(`/api/admin/v1/users/${userId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!userResponse.ok()) { + const errorText = await userResponse.text(); + console.error(`[MAS API] Failed to get user details: ${userResponse.status()} - ${errorText}`); + throw new Error(`Failed to get MAS user details: ${userResponse.status()} - ${errorText}`); + } - if (!userResponse.ok()) { - const errorText = await userResponse.text(); - console.error( - `[MAS API] Failed to get user details: ${userResponse.status()} - ${errorText}` - ); - throw new Error(`Failed to get MAS user details: ${userResponse.status()} - ${errorText}`); - } + const userResult = await userResponse.json(); + const user = userResult.data; - const userResult = await userResponse.json(); - const user = userResult.data; + //console.log(`[MAS API] User found: Yes`); + console.log( + `[MAS API] User found : ID: ${user.id}, Username: ${user.attributes.username || 'N/A'}` + ); - console.log( - `[MAS API] User found : ID: ${user.id}, Username: ${user.attributes.username || 'N/A'}` - ); + return user; +} - return user; +/** + * Check if a user exists in MAS by email + */ +export async function checkMasUserExistsByEmail(email: string): Promise { + console.log(`[MAS API] Checking if user exists with email: ${email}`); + try { + const user = await getMasUserByEmail(email); + const exists = user !== null; + //console.log(`[MAS API] User with email ${email} exists: ${exists}`); + return exists; + } catch (error) { + console.error(`[MAS API] Error checking user existence: ${error}`); + return false; } +} - /** - * Check if a user exists in MAS by email - */ - public async checkUserExistsByEmail(email: string): Promise { - console.log(`[MAS API] Checking if user exists with email: ${email}`); +/** + * Wait for a user to be created in MAS + * This is useful after OIDC authentication, as there might be a slight delay + * before the user is fully created in MAS + */ +export async function waitForMasUser( + email: string, + maxAttempts = 10, + delayMs = 1000 +): Promise { + console.log( + `[MAS API] Waiting for user with email ${email} to be created (max ${maxAttempts} attempts)` + ); + for (let attempt = 0; attempt < maxAttempts; attempt++) { + console.log(`[MAS API] Attempt ${attempt + 1}/${maxAttempts} to find user`); try { - const user = await this.getUserByEmail(email); - return user !== null; - } catch (error) { - console.error(`[MAS API] Error checking user existence: ${error}`); - return false; - } - } - - /** - * Wait for a user to be created in MAS - */ - public async waitForUser(email: string, maxAttempts = 10, delayMs = 1000): Promise { - console.log( - `[MAS API] Waiting for user with email ${email} to be created (max ${maxAttempts} attempts)` - ); - for (let attempt = 0; attempt < maxAttempts; attempt++) { - console.log(`[MAS API] Attempt ${attempt + 1}/${maxAttempts} to find user`); - try { - const user = await this.getUserByEmail(email); - if (user) { - console.log(`[MAS API] User found on attempt ${attempt + 1}`); - return user; - } - console.log( - `[MAS API] User not found on attempt ${attempt + 1}, waiting ${delayMs}ms before next attempt` - ); - } catch (error) { - console.warn(`[MAS API] Attempt ${attempt + 1}/${maxAttempts} failed: ${error}`); + const user = await getMasUserByEmail(email); + if (user) { + console.log(`[MAS API] User found on attempt ${attempt + 1}`); + return user; } - - // Wait before the next attempt - await new Promise((resolve) => setTimeout(resolve, delayMs)); + console.log( + `[MAS API] User not found on attempt ${attempt + 1}, waiting ${delayMs}ms before next attempt` + ); + } catch (error) { + console.warn(`[MAS API] Attempt ${attempt + 1}/${maxAttempts} failed: ${error}`); } - const errorMsg = `User with email ${email} not found in MAS after ${maxAttempts} attempts`; - console.error(`[MAS API] ${errorMsg}`); - throw new Error(errorMsg); + // Wait before the next attempt + await new Promise((resolve) => setTimeout(resolve, delayMs)); } - /** - * Create a user in MAS with a password - * @returns masId - */ - public async createUserWithPassword( - username: string, - email: string, - password: string, - displayName?: string - ): Promise { - console.log( - `[MAS API] Creating user with username:${username}, email:${email}, password:${password}, displayName:${displayName || 'No displayname'}` - ); - const token = await this.getToken(); + const errorMsg = `User with email ${email} not found in MAS after ${maxAttempts} attempts`; + console.error(`[MAS API] ${errorMsg}`); + throw new Error(errorMsg); +} - const createUserData = { +/** + * Create a user in MAS with a password + */ +export async function createMasUserWithPassword( + username: string, + email: string, + password: string +): Promise { + console.log( + `[MAS API] Creating user with username:${username}, email:${email}, password:${password}` + ); + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.post('/api/admin/v1/users', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + data: { username: username, skip_homeserver_check: false, - ...(displayName && { displayname: displayName }), - }; + }, + }); - const response = await this.apiContext.post('/api/admin/v1/users', { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - data: createUserData, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to create user: ${response.status()} - ${errorText}`); - throw new Error(`Failed to create MAS user: ${response.status()} - ${errorText}`); - } - - const data = await response.json(); - const masId = data.data.id; + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to create user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to create MAS user: ${response.status()} - ${errorText}`); + } - const responsePwd = await this.apiContext.post(`/api/admin/v1/users/${masId}/set-password`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - data: { - password: password, - skip_password_check: true, - }, - }); + const data = await response.json(); + //console.log(data.data) + const userId = data.data.id; + + const responsePwd = await apiRequestContext.post(`/api/admin/v1/users/${userId}/set-password`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + data: { + password: password, + skip_password_check: true, + }, + }); + + if (!responsePwd.ok()) { + const errorText = await responsePwd.text(); + console.error( + `[MAS API] Failed to set password for user: ${responsePwd.status()} - ${errorText}` + ); + throw new Error(`Failed to set password for user: ${responsePwd.status()} - ${errorText}`); + } - if (!responsePwd.ok()) { - const errorText = await responsePwd.text(); - console.error( - `[MAS API] Failed to set password for user: ${responsePwd.status()} - ${errorText}` - ); - throw new Error(`Failed to set password for user: ${responsePwd.status()} - ${errorText}`); - } + const responseEmail = await apiRequestContext.post(`/api/admin/v1/user-emails`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + data: { + user_id: userId, + email: email, + }, + }); + + if (!responseEmail.ok()) { + const errorText = await responseEmail.text(); + console.error( + `[MAS API] Failed to set email for user: ${responseEmail.status()} - ${errorText}` + ); + throw new Error(`Failed to set email for user: ${responseEmail.status()} - ${errorText}`); + } - const responseEmail = await this.apiContext.post(`/api/admin/v1/user-emails`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - data: { - user_id: masId, - email: email, - }, - }); + // Verify the user exists in MAS + const existsBeforeLogin = await checkMasUserExistsByEmail(email); - if (!responseEmail.ok()) { - const errorText = await responseEmail.text(); - console.error( - `[MAS API] Failed to set email for user: ${responseEmail.status()} - ${errorText}` - ); - throw new Error(`Failed to set email for user: ${responseEmail.status()} - ${errorText}`); - } + console.log(`[MAS API] User created successfully with ID: ${userId}`); + return existsBeforeLogin ? userId : 'error'; +} - console.log(`[MAS API] User created successfully with ID: ${masId}`); - return masId; +/** + * Delete a user from MAS + */ +export async function deactivateMasUser(userId: string): Promise { + console.log(`[MAS API] Deleting user with ID: ${userId}`); + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.post(`/api/admin/v1/users/${userId}/deactivate`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to deactivate user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to deactivate MAS user: ${response.status()} - ${errorText}`); } - /** - * Delete a user from MAS - */ - public async deactivateUser(userId: string): Promise { - console.log(`[MAS API] Deleting user with ID: ${userId}`); - const token = await this.getToken(); + console.log(`[MAS API] User deactivated successfully`); +} + +/** + * Check if a oauth link exists + */ +export async function oauthLinkExistsByUserId(userId: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); - const response = await this.apiContext.post(`/api/admin/v1/users/${userId}/deactivate`, { + const response = await apiRequestContext.get( + `/api/admin/v1/upstream-oauth-links?filter[user]=${userId}`, + { headers: { Authorization: `Bearer ${token}`, }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to deactivate user: ${response.status()} - ${errorText}`); - throw new Error(`Failed to deactivate MAS user: ${response.status()} - ${errorText}`); } + ); - console.log(`[MAS API] User deactivated successfully`); + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to delete user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to delete MAS user: ${response.status()} - ${errorText}`); } + const data = await response.json(); + //console.log(data.data) + const links = data.data; + console.log(`[MAS API] Oauth links for user ${userId} : ${JSON.stringify(links)}`); + return links.length === 1; +} - /** - * Reactivate a user from MAS - */ - public async reactivateUser(userId: string): Promise { - console.log(`[MAS API] Reactivating user with ID: ${userId}`); - const token = await this.getToken(); +/** + * Check if a oauth link exists + */ +export async function oauthLinkExistsBySubject(subject: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); - const response = await this.apiContext.post(`/api/admin/v1/users/${userId}/reactivate`, { + const response = await apiRequestContext.get( + `/api/admin/v1/upstream-oauth-links?filter[subject]=${subject}`, + { headers: { Authorization: `Bearer ${token}`, }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to reactivate user: ${response.status()} - ${errorText}`); - throw new Error(`Failed to reactivate MAS user: ${response.status()} - ${errorText}`); - } - - console.log(`[MAS API] User reactivated successfully`); - } - - /** - * Check if a oauth link exists by user ID - */ - public async oauthLinkExistsByUserId(userId: string): Promise { - const token = await this.getToken(); - - const response = await this.apiContext.get( - `/api/admin/v1/upstream-oauth-links?filter[user]=${userId}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to check oauth links: ${response.status()} - ${errorText}`); - throw new Error(`Failed to check oauth links: ${response.status()} - ${errorText}`); - } - - const data = await response.json(); - const links = data.data; - console.log(`[MAS API] Oauth links for user ${userId} : ${JSON.stringify(links)}`); - return links.length === 1; - } - - /** - * Check if a oauth link exists by subject - */ - public async oauthLinkExistsBySubject(subject: string): Promise { - const token = await this.getToken(); - - const response = await this.apiContext.get( - `/api/admin/v1/upstream-oauth-links?filter[subject]=${subject}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to check oauth links: ${response.status()} - ${errorText}`); - throw new Error(`Failed to check oauth links: ${response.status()} - ${errorText}`); - } - - const data = await response.json(); - const links = data.data; - console.log(`[MAS API] Oauth links for subject ${subject} : ${JSON.stringify(links)}`); - return links.length === 1; - } - - /** - * Get oauth links by user ID - */ - public async getOauthLinksByUserId(userId: string): Promise { - const token = await this.getToken(); - - const response = await this.apiContext.get( - `/api/admin/v1/upstream-oauth-links?filter[user]=${userId}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to get oauth links: ${response.status()} - ${errorText}`); - throw new Error(`Failed to get oauth links: ${response.status()} - ${errorText}`); } + ); - const data = await response.json(); - const links = data.data; - console.log(`[MAS API] Oauth links for user ${userId} : ${JSON.stringify(links)}`); - return links; + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to delete user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to delete MAS user: ${response.status()} - ${errorText}`); } + const data = await response.json(); + //console.log(data.data) + const links = data.data; + console.log(`[MAS API] Oauth links for user ${subject} : ${JSON.stringify(links)}`); + return links.length === 1; +} - /** - * Get oauth links by subject - */ - public async getOauthLinksBySubject(subject: string): Promise { - const token = await this.getToken(); - - const response = await this.apiContext.get( - `/api/admin/v1/upstream-oauth-links?filter[subject]=${subject}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to get oauth links: ${response.status()} - ${errorText}`); - throw new Error(`Failed to get oauth links: ${response.status()} - ${errorText}`); - } - - const data = await response.json(); - const links = data.data; - console.log(`[MAS API] Oauth links for subject ${subject} : ${JSON.stringify(links)}`); - return links; +/** + * Dispose the API context when done + */ +export async function disposeApiContext(): Promise { + if (apiContext) { + //console.log(`[MAS API] Disposing API context`); + await apiContext.dispose(); + apiContext = null; + //console.log(`[MAS API] API context disposed`); + } else { + console.log(`[MAS API] No API context to dispose`); } +} - /** - * Delete an oauth link - */ - public async deleteOauthLink(id: string): Promise { - const token = await this.getToken(); +/** + * Check if a oauth link exists + */ +export async function getOauthLinkByUserId(userId: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); - const response = await this.apiContext.delete(`/api/admin/v1/upstream-oauth-links/${id}`, { + const response = await apiRequestContext.get( + `/api/admin/v1/upstream-oauth-links?filter[user]=${userId}`, + { headers: { Authorization: `Bearer ${token}`, }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to delete oauth link: ${response.status()} - ${errorText}`); - throw new Error(`Failed to delete oauth link: ${response.status()} - ${errorText}`); } + ); - console.log(`[MAS API] Oauth link deleted for id:${id}`); + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to delete user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to delete MAS user: ${response.status()} - ${errorText}`); } + const data = await response.json(); + //console.log(data.data) + const links = data.data; + console.log(`[MAS API] Oauth links for user ${userId} : ${JSON.stringify(links)}`); + return links; +} - /** - * Add an email to a user - */ - public async addUserEmail(userId: string, email: string): Promise { - const token = await this.getToken(); +export async function getOauthLinkBySubject(subject: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); - const response = await this.apiContext.post(`/api/admin/v1/user-emails`, { + const response = await apiRequestContext.get( + `/api/admin/v1/upstream-oauth-links?filter[subject]=${subject}`, + { headers: { - 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - data: { - user_id: userId, - email: email, - }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to add email for user: ${response.status()} - ${errorText}`); - throw new Error(`Failed to add email for user: ${response.status()} - ${errorText}`); } + ); - console.log(`[MAS API] User email added for user_id:${userId}`); + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to delete user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to delete MAS user: ${response.status()} - ${errorText}`); } + const data = await response.json(); + //console.log(data.data) + const links = data.data; + console.log(`[MAS API] Oauth links for user ${subject} : ${JSON.stringify(links)}`); + return links; +} - /** - * Get user email by user ID and email - */ - public async getUserEmail(userId: string, email: string): Promise { - const token = await this.getToken(); - - const response = await this.apiContext.get( - `/api/admin/v1/user-emails?filter[user]=${userId}&filter[email]=${email}`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - } - ); +export async function deleteOauthLink(id: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); - if (!response.ok()) { - const errorText = await response.text(); - console.error(`[MAS API] Failed to get email for user: ${response.status()} - ${errorText}`); - throw new Error(`Failed to get email for user: ${response.status()} - ${errorText}`); - } + const response = await apiRequestContext.delete(`/api/admin/v1/upstream-oauth-links/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); - const json = await response.json(); - const user_email = json.data[0]; - console.log( - `[MAS API] User email retrieved for userId:${userId}, user_email : ${JSON.stringify(user_email)}` - ); - return user_email; + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to delete user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to delete MAS user: ${response.status()} - ${errorText}`); } + console.log( + `[MAS API] Oauth links deleted for id:${id}, response : ${JSON.stringify(response)} ` + ); + return; +} - /** - * Delete a user email - */ - public async deleteUserEmail(id: string): Promise { - const token = await this.getToken(); +export async function addUserEmail(userId: string, email: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.post(`/api/admin/v1/user-emails`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + data: { + user_id: userId, + email: email, + }, + }); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to set email for user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to set email for user: ${response.status()} - ${errorText}`); + } + console.log( + `[MAS API] User email added for user_id:${userId}, response : ${JSON.stringify(response)} ` + ); + return; +} - const response = await this.apiContext.delete(`/api/admin/v1/user-emails/${id}`, { +export async function getUserEmail(userId: string, email: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.get( + `/api/admin/v1/user-emails?filter[user]=${userId}&filter[email]=${email}`, + { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - }); - - if (!response.ok()) { - const errorText = await response.text(); - console.error( - `[MAS API] Failed to delete email for user: ${response.status()} - ${errorText}` - ); - throw new Error(`Failed to delete email for user: ${response.status()} - ${errorText}`); } + ); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to set email for user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to set email for user: ${response.status()} - ${errorText}`); + } + const json = await response.json(); + const user_email = json.data[0]; + console.log( + `[MAS API] User email retrieved for userId:${userId}, user_email : ${JSON.stringify(user_email)} ` + ); + return user_email; +} - console.log(`[MAS API] User email deleted for user-emails:${id}`); +export async function deleteUserEmail(id: string): Promise { + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.delete(`/api/admin/v1/user-emails/${id}`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to set email for user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to set email for user: ${response.status()} - ${errorText}`); } + console.log( + `[MAS API] User email deleted for user-emails:${id}, response : ${JSON.stringify(response)} ` + ); + return; } -// Export the MasAdminClient class for direct use -export { MasAdminClient }; +/** + * Reactivate a user from MAS + */ +export async function reactivateMasUser(userId: string): Promise { + console.log(`[MAS API] Deleting user with ID: ${userId}`); + const token = await getMasAdminToken(); + const apiRequestContext = await getApiContext(); + + const response = await apiRequestContext.post(`/api/admin/v1/users/${userId}/reactivate`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok()) { + const errorText = await response.text(); + console.error(`[MAS API] Failed to reactivate user: ${response.status()} - ${errorText}`); + throw new Error(`Failed to reactivate MAS user: ${response.status()} - ${errorText}`); + } + + console.log(`[MAS API] User reactivated successfully`); +} diff --git a/utils/matrix-api.ts b/utils/matrix-api.ts deleted file mode 100644 index ca06982..0000000 --- a/utils/matrix-api.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { createClient, type StateEvents, type MatrixClient, EventType } from 'matrix-js-sdk'; -import { Credentials } from './auth-helpers'; - -export interface AccessRules { - rule: 'restricted' | 'direct' | 'unrestricted'; - force_unencrypted_at_creation?: boolean; - visibility?: 'public' | 'private'; -} - -export interface RoomCreationOptions { - name: string; - topic?: string; - accessRules?: AccessRules; - encryption?: boolean; - visibility?: 'public' | 'private'; - joinRule?: 'invite' | 'knock' | 'public' | 'private'; - preset: 'public_chat' | 'private_chat' | 'trusted_private_chat'; - is_direct?: boolean; - power_level_content_override?: any; - creation_content?: any; - room_version?: string; - retention_max_lifetime?: number; -} - -export const AccessRulesEventType = 'im.vector.room.access_rules'; - -export class MatrixApi { - private client: MatrixClient; - private matrixUrl: string; - - public constructor(matrixUrl: string, masUrl: string) { - this.matrixUrl = matrixUrl; - // create client with MAS URL to ease local login on preprod server - this.client = createClient({ - baseUrl: `${masUrl}`, - }); - } - - /** - * Login a user - * @ - */ - public async login(username: string, password: string): Promise { - const response = await this.client.loginRequest({ - type: 'm.login.password', - user: username, - password: password, - }); - - // Create a new MatrixClient instance with the token - this.client = createClient({ - baseUrl: `${this.matrixUrl}`, - accessToken: response.access_token, - userId: response.user_id, - deviceId: response.device_id, - }); - - return { - homeserverBaseUrl: this.client.getHomeserverUrl(), - password, - accessToken: response.access_token, - userId: response.user_id, - deviceId: response.device_id, - homeServer: response.home_server || response.user_id.split(':').slice(1).join(':'), - username: username.slice(1).split(':')[0], - }; - } - - /** - * Create a room - */ - public async createRoom(options: RoomCreationOptions): Promise { - const initialState: any[] = []; - - // Add join_rule - if (options.joinRule) { - initialState.push({ - type: EventType.RoomJoinRules, - state_key: '', - content: { join_rule: options.joinRule }, - }); - } - - // Add access control rules - if (options.accessRules) { - const content: any = {}; - if (options.accessRules.rule) { - content.rule = options.accessRules.rule; - } - if (options.accessRules.force_unencrypted_at_creation !== undefined) { - content.force_unencrypted_at_creation = options.accessRules.force_unencrypted_at_creation; - } - if (options.accessRules.visibility) { - content.visibility = options.accessRules.visibility; - } - - initialState.push({ - type: AccessRulesEventType, - state_key: '', - content: content, - }); - } - - if (options.retention_max_lifetime) { - initialState.push({ - type: 'm.room.retention', - state_key: '', - content: { max_lifetime: options.retention_max_lifetime }, - }); - } - - const response = await this.client.createRoom({ - name: options.name, - topic: options.topic, - visibility: options.visibility as any, - preset: options.preset as any, - initial_state: initialState as any, - is_direct: options.is_direct, - power_level_content_override: options.power_level_content_override, - creation_content: options.creation_content, - room_version: options.room_version, - }); - - return response.room_id; - } - - public async upgradeRoom( - roomId: string, - newVersion: string - ): Promise<{ - replacement_room: string; - }> { - return await this.client.upgradeRoom(roomId, newVersion); - } - - - /** - * Check if room is encrypted - */ - public async isRoomEncrypted(roomId: string): Promise { - try { - return await this.client.getStateEvent(roomId, EventType.RoomEncryption, '') !== null; - } catch (e) { - return false; - } - } - - /** - * Get join rule - */ - public async getJoinRule(roomId: string): Promise { - try { - return (await this.client.getStateEvent(roomId, EventType.RoomJoinRules, ''))?.join_rule || null; - } catch (e) { - console.log('event join rules not found'); - return null; - } - } - - /** - * Get access rules - */ - public async getAccessRulesEvent(roomId: string): Promise | null> { - try { - return await this.client.getStateEvent(roomId, AccessRulesEventType, ''); - } catch (e) { - console.log('event access rules not found'); - return null; - } - } - - /** - * Get room retention - */ - public async getRoomRetentionEvent(roomId: string): Promise | null> { - try { - return await this.client.getStateEvent(roomId, 'm.room.retention', ''); - } catch (e) { - console.log('event room retention not found'); - return null; - } - } - - /** - * Send a state event to a room - */ - public async sendStateEvent( - roomId: string, - eventType: K, - content: StateEvents[K], - stateKey: string = '' - ): Promise { - return this.client.sendStateEvent(roomId, eventType, content, stateKey); - } - - /** - * Logout - */ - public async logout(): Promise { - await this.client.logout(); - } - - /** - * get authenticated Matrix client - * @returns MatrixClient - */ - public getClient() { - return this.client; - } -} diff --git a/utils/synapse-admin.ts b/utils/synapse-admin.ts index 63dc32a..260d405 100644 --- a/utils/synapse-admin.ts +++ b/utils/synapse-admin.ts @@ -1,5 +1,5 @@ import type { APIRequestContext } from '@playwright/test'; -import { SYNAPSE_ADMIN_TOKEN, MATRIX_URL } from './config'; +import { SYNAPSE_ADMIN_TOKEN, BASE_URL } from './config'; /** * Helper function to set account expiration using the Synapse admin API @@ -18,20 +18,17 @@ export async function setAccountExpiration( ): Promise { console.log(`[Synapse API] Setting expiration for user: ${userId} to timestamp: ${expirationTs}`); - const response = await request.post( - `${MATRIX_URL}/_synapse/client/email_account_validity/admin`, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${SYNAPSE_ADMIN_TOKEN}`, - }, - data: { - user_id: userId, - expiration_ts: expirationTs, - enable_renewal_emails: enableRenewalEmails, - }, - } - ); + const response = await request.post(`${BASE_URL}/_synapse/client/email_account_validity/admin`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${SYNAPSE_ADMIN_TOKEN}`, + }, + data: { + user_id: userId, + expiration_ts: expirationTs, + enable_renewal_emails: enableRenewalEmails, + }, + }); if (!response.ok()) { const errorText = await response.text();