Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
876 lines
36 KiB
TypeScript
876 lines
36 KiB
TypeScript
import child_process from 'child_process';
|
|
import path from 'path';
|
|
|
|
import { faker } from '@faker-js/faker';
|
|
import type { Page } from '@playwright/test';
|
|
import { v2 as compose } from 'docker-compose';
|
|
import { MongoClient } from 'mongodb';
|
|
|
|
import * as constants from './config/constants';
|
|
import { createUserFixture } from './fixtures/collections/users';
|
|
import { Users } from './fixtures/userStates';
|
|
import { Registration } from './page-objects';
|
|
import { convertHexToRGB } from './utils/convertHexToRGB';
|
|
import { createCustomRole, deleteCustomRole } from './utils/custom-role';
|
|
import { getUserInfo } from './utils/getUserInfo';
|
|
import { parseMeteorResponse } from './utils/parseMeteorResponse';
|
|
import { saveSettings } from './utils/saveSettings';
|
|
import { setSettingValueById } from './utils/setSettingValueById';
|
|
import type { BaseTest } from './utils/test';
|
|
import { test, expect } from './utils/test';
|
|
|
|
const KEY = 'fuselage-sessionStorage-saml_invite_token';
|
|
|
|
const resetTestData = async ({ api, cleanupOnly = false }: { api?: any; cleanupOnly?: boolean } = {}) => {
|
|
// Reset saml users' data on mongo in the beforeAll hook to allow re-running the tests within the same playwright session
|
|
// This is needed because those tests will modify this data and running them a second time would trigger different code paths
|
|
const connection = await MongoClient.connect(constants.URL_MONGODB);
|
|
|
|
const usernamesToDelete = [
|
|
...[
|
|
Users.userForSamlMerge,
|
|
Users.userForSamlMerge2,
|
|
Users.samluser1,
|
|
Users.samluser2,
|
|
Users.samluser4,
|
|
Users.samluser5,
|
|
Users.samluser6,
|
|
Users.samluser7,
|
|
Users.samlusernoname,
|
|
Users.samlusernoname2,
|
|
].map(({ data: { username } }) => username),
|
|
'custom_saml_username',
|
|
'custom_saml_username2',
|
|
];
|
|
|
|
await connection
|
|
.db()
|
|
.collection('users')
|
|
.deleteMany({
|
|
username: {
|
|
$in: usernamesToDelete,
|
|
},
|
|
});
|
|
|
|
if (cleanupOnly) {
|
|
return;
|
|
}
|
|
|
|
const usersFixtures = [Users.userForSamlMerge, Users.userForSamlMerge2].map((user) => createUserFixture(user));
|
|
await Promise.all(
|
|
usersFixtures.map((user) =>
|
|
connection.db().collection('users').updateOne({ username: user.username }, { $set: user }, { upsert: true }),
|
|
),
|
|
);
|
|
|
|
const settings = [
|
|
{ _id: 'Accounts_AllowAnonymousRead', value: false },
|
|
{ _id: 'Accounts_AllowUsernameChange', value: true },
|
|
{ _id: 'SAML_Custom_Default_logout_behaviour', value: 'SAML' },
|
|
{ _id: 'SAML_Custom_Default_immutable_property', value: 'EMail' },
|
|
{ _id: 'SAML_Custom_Default_mail_overwrite', value: false },
|
|
{ _id: 'SAML_Custom_Default_name_overwrite', value: false },
|
|
{ _id: 'SAML_Custom_Default', value: false },
|
|
...(constants.IS_EE
|
|
? [
|
|
{ _id: 'SAML_Custom_Default_role_attribute_sync', value: true },
|
|
{ _id: 'SAML_Custom_Default_role_attribute_name', value: 'role' },
|
|
]
|
|
: []),
|
|
{ _id: 'SAML_Custom_Default_user_data_fieldmap', value: '{"username":"username", "email":"email", "name": "cn"}' },
|
|
{ _id: 'SAML_Custom_Default_provider', value: 'test-sp' },
|
|
{ _id: 'SAML_Custom_Default_issuer', value: 'http://localhost:3000/_saml/metadata/test-sp' },
|
|
{ _id: 'SAML_Custom_Default_entry_point', value: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php' },
|
|
{ _id: 'SAML_Custom_Default_idp_slo_redirect_url', value: 'http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php' },
|
|
{ _id: 'SAML_Custom_Default_button_label_text', value: 'SAML test login button' },
|
|
{ _id: 'SAML_Custom_Default_button_color', value: '#185925' },
|
|
{ _id: 'SAML_Custom_Default_signature_validation_type', value: 'All' },
|
|
{ _id: 'SAML_Custom_Default_validate_logout_request_signature', value: true },
|
|
{ _id: 'SAML_Custom_Default_validate_logout_response_signature', value: true },
|
|
{
|
|
_id: 'SAML_Custom_Default_cert',
|
|
value: `MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
|
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
|
aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF
|
|
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
|
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
|
CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav
|
|
Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+
|
|
YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc
|
|
+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix
|
|
YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8
|
|
jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C
|
|
YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw
|
|
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b
|
|
lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs
|
|
X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7
|
|
yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7
|
|
NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
|
|
99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n
|
|
aQ==`,
|
|
},
|
|
];
|
|
|
|
await saveSettings(api, settings);
|
|
};
|
|
|
|
const setupCustomRole = async (api: BaseTest['api']) => {
|
|
const roleResponse = await createCustomRole(api, { name: 'saml-role' });
|
|
expect(roleResponse.status()).toBe(200);
|
|
|
|
const { role } = await roleResponse.json();
|
|
return role._id;
|
|
};
|
|
|
|
test.describe('SAML', () => {
|
|
let poRegistration: Registration;
|
|
let samlRoleId: string;
|
|
let targetInviteGroupId: string;
|
|
let targetInviteGroupName: string;
|
|
let inviteId: string;
|
|
let targetChannel: string;
|
|
let autoCreatedChannel: string;
|
|
|
|
const containerPath = path.join(__dirname, 'containers', 'saml');
|
|
|
|
test.beforeAll(async ({ api }) => {
|
|
await resetTestData({ api });
|
|
|
|
// Only one setting updated through the API to avoid refreshing the service configurations several times
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default', true)).status()).toBe(200);
|
|
|
|
// Create a new custom role
|
|
if (constants.IS_EE) {
|
|
samlRoleId = await setupCustomRole(api);
|
|
}
|
|
|
|
await compose.buildOne('testsamlidp_idp', {
|
|
cwd: containerPath,
|
|
});
|
|
|
|
await compose.upOne('testsamlidp_idp', {
|
|
cwd: containerPath,
|
|
});
|
|
});
|
|
|
|
test.beforeAll(async ({ api }) => {
|
|
const [groupResponse, channelResponse] = await Promise.all([
|
|
api.post('/groups.create', { name: faker.string.uuid() }),
|
|
api.post('/channels.create', { name: 'saml-channel-1' }),
|
|
]);
|
|
|
|
expect(groupResponse.status()).toBe(200);
|
|
expect(channelResponse.status()).toBe(200);
|
|
|
|
const { group } = await groupResponse.json();
|
|
targetInviteGroupId = group._id;
|
|
targetInviteGroupName = group.name;
|
|
targetChannel = 'saml-channel-1';
|
|
|
|
const inviteResponse = await api.post('/findOrCreateInvite', { rid: targetInviteGroupId, days: 1, maxUses: 0 });
|
|
expect(inviteResponse.status()).toBe(200);
|
|
const { _id } = await inviteResponse.json();
|
|
inviteId = _id;
|
|
});
|
|
|
|
test.afterAll(async ({ api }) => {
|
|
await compose.down({
|
|
cwd: containerPath,
|
|
});
|
|
|
|
// the compose CLI doesn't have any way to remove images, so try to remove it with a direct call to the docker cli, but ignore errors if it fails.
|
|
try {
|
|
child_process.spawn('docker', ['rmi', 'saml-testsamlidp_idp'], {
|
|
cwd: containerPath,
|
|
});
|
|
} catch {
|
|
// ignore errors here
|
|
}
|
|
|
|
// Remove saml test users so they don't interfere with other tests
|
|
await resetTestData({ cleanupOnly: true });
|
|
|
|
// Remove created custom role
|
|
if (constants.IS_EE) {
|
|
expect((await deleteCustomRole(api, 'saml-role')).status()).toBe(200);
|
|
}
|
|
});
|
|
|
|
test.afterAll(async ({ api }) => {
|
|
await Promise.all([
|
|
api.post('/groups.delete', { roomId: targetInviteGroupId }).then((response) => expect(response.status()).toBe(200)),
|
|
api.post('/channels.delete', { roomName: targetChannel }).then((response) => expect(response.status()).toBe(200)),
|
|
api.post('/channels.delete', { roomName: autoCreatedChannel }),
|
|
]);
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
poRegistration = new Registration(page);
|
|
|
|
await poRegistration.goto();
|
|
});
|
|
|
|
test('Login', async ({ page, api }) => {
|
|
await test.step('expect to have SAML login button available', async () => {
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
await test.step('expect to have SAML login button to have the required background color', async () => {
|
|
await expect(poRegistration.btnLoginWithSaml).toHaveCSS('background-color', convertHexToRGB('#185925'));
|
|
});
|
|
|
|
await test.step('expect to be redirected to the IdP for login', async () => {
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
|
|
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
});
|
|
|
|
await test.step('expect to be redirected back on successful login', async () => {
|
|
await page.getByLabel('Username').fill('samluser1');
|
|
await page.getByLabel('Password').fill('password');
|
|
await page.locator('role=button[name="Login"]').click();
|
|
|
|
await expect(page).toHaveURL('/home');
|
|
});
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'samluser1');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('samluser1');
|
|
expect(user?.name).toBe('Saml User 1');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser1@example.com');
|
|
});
|
|
});
|
|
|
|
test('Allow password change for OAuth users', async ({ api }) => {
|
|
await test.step("should not send password reset mail if 'Allow Password Change for OAuth Users' setting is disabled", async () => {
|
|
expect((await setSettingValueById(api, 'Accounts_AllowPasswordChangeForOAuthUsers', false)).status()).toBe(200);
|
|
|
|
const response = await api.post('/method.call/sendForgotPasswordEmail', {
|
|
message: JSON.stringify({ msg: 'method', id: 'id', method: 'sendForgotPasswordEmail', params: ['samluser1@example.com'] }),
|
|
});
|
|
const mailSent = await parseMeteorResponse<boolean>(response);
|
|
expect(response.status()).toBe(200);
|
|
expect(mailSent).toBe(false);
|
|
});
|
|
|
|
await test.step("should send password reset mail if 'Allow Password Change for OAuth Users' setting is enabled", async () => {
|
|
expect((await setSettingValueById(api, 'Accounts_AllowPasswordChangeForOAuthUsers', true)).status()).toBe(200);
|
|
|
|
const response = await api.post('/method.call/sendForgotPasswordEmail', {
|
|
message: JSON.stringify({ msg: 'method', id: 'id', method: 'sendForgotPasswordEmail', params: ['samluser1@example.com'] }),
|
|
});
|
|
const mailSent = await parseMeteorResponse<boolean>(response);
|
|
expect(response.status()).toBe(200);
|
|
expect(mailSent).toBe(true);
|
|
});
|
|
});
|
|
|
|
const doChangeSetting = async (api: any, settingId: string, settingValue: any) => {
|
|
await expect((await setSettingValueById(api, settingId, settingValue)).status()).toBe(200);
|
|
};
|
|
|
|
const doLoginStep = async (page: Page, username: string, redirectUrl: string | null = '/home') => {
|
|
await test.step('expect successful login', async () => {
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
// Redirect to Idp
|
|
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
|
|
// Fill username and password
|
|
await page.getByLabel('Username').fill(username);
|
|
await page.getByLabel('Password').fill('password');
|
|
await page.locator('role=button[name="Login"]').click();
|
|
|
|
// Redirect back to rocket.chat
|
|
if (redirectUrl) {
|
|
await expect(page).toHaveURL(redirectUrl);
|
|
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
|
|
}
|
|
});
|
|
};
|
|
|
|
const doLoginStepWithUsernameSelection = async (page: Page, username: string) => {
|
|
await test.step('expect successful login without username to show username selection screen', async () => {
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
// Redirect to Idp
|
|
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
|
|
// Fill username and password
|
|
await page.getByLabel('Username').fill(username);
|
|
await page.getByLabel('Password').fill('password');
|
|
await page.locator('role=button[name="Login"]').click();
|
|
|
|
// Should redirect to username selection screen
|
|
await expect(poRegistration.username).toBeVisible();
|
|
});
|
|
};
|
|
|
|
const doUsernameSelection = async (customUsername: string) => {
|
|
await poRegistration.username.fill(customUsername);
|
|
await poRegistration.btnRegisterConfirmUsername.click();
|
|
};
|
|
|
|
const doLogoutStep = async (page: Page) => {
|
|
await test.step('logout', async () => {
|
|
await page.getByRole('button', { name: 'User menu' }).click();
|
|
await page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]').click();
|
|
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).not.toBeVisible();
|
|
});
|
|
};
|
|
|
|
const expectLoggedOutAfterLogoutRequest = async (page: Page) => {
|
|
await test.step('expect user to be logged out from Rocket.Chat', async () => {
|
|
// The logout response echoes back the RelayState from the request (per the SAML spec), so the browser
|
|
// lands on the IdP after SLO instead of returning here; navigate back to confirm the session ended.
|
|
await poRegistration.goto();
|
|
await expect(page.getByRole('button', { name: 'User menu' })).not.toBeVisible();
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible();
|
|
});
|
|
};
|
|
|
|
test('Logout - Rocket.Chat only', async ({ page, api }) => {
|
|
await test.step('Configure logout to only logout from Rocket.Chat', async () => {
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'Local')).status()).toBe(200);
|
|
});
|
|
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
await doLogoutStep(page);
|
|
|
|
await test.step('expect IdP to redirect back automatically on new login request', async () => {
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
|
|
await expect(page).toHaveURL('/home');
|
|
});
|
|
});
|
|
|
|
test('Logout - Single Sign Out', async ({ page, api }) => {
|
|
await test.step('Configure logout to terminate SAML session', async () => {
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'SAML')).status()).toBe(200);
|
|
});
|
|
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
await doLogoutStep(page);
|
|
|
|
await test.step('expect IdP to show login form on new login request', async () => {
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
|
|
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
await expect(page.getByLabel('Username')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('Logout - From IdP', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
// This should trigger a logout request from the IdP, with a redirect to our home on success
|
|
await page.goto('http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php?ReturnTo=http://localhost:3000');
|
|
|
|
await test.step('expect user to be logged out from Rocket.Chat', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).not.toBeVisible();
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('User Merge - By Email', async ({ page, api }) => {
|
|
await test.step('Configure SAML to identify users by email', async () => {
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser2');
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'samluser2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?._id).toBe('user_for_saml_merge');
|
|
expect(user?.username).toBe('samluser2');
|
|
expect(user?.name).toBe('user_for_saml_merge');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com');
|
|
});
|
|
});
|
|
|
|
test('User Merge - By Email with Name Override', async ({ page, api }) => {
|
|
await test.step('Configure SAML to identify users by email', async () => {
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200);
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', true)).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser2');
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'samluser2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?._id).toBe('user_for_saml_merge');
|
|
expect(user?.username).toBe('samluser2');
|
|
expect(user?.name).toBe('Saml User 2');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com');
|
|
});
|
|
});
|
|
|
|
test('User Merge - By Username', async ({ page, api }) => {
|
|
await test.step('Configure SAML to identify users by username', async () => {
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', false)).status()).toBe(200);
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', false)).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser3');
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'user_for_saml_merge2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?._id).toBe('user_for_saml_merge2');
|
|
expect(user?.username).toBe('user_for_saml_merge2');
|
|
expect(user?.name).toBe('user_for_saml_merge2');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('user_for_saml_merge2@email.com');
|
|
});
|
|
});
|
|
|
|
test('User Merge - By Username with Email Override', async ({ page, api }) => {
|
|
await test.step('Configure SAML to identify users by username', async () => {
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', false)).status()).toBe(200);
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', true)).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser3');
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'user_for_saml_merge2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?._id).toBe('user_for_saml_merge2');
|
|
expect(user?.username).toBe('user_for_saml_merge2');
|
|
expect(user?.name).toBe('user_for_saml_merge2');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser3@example.com');
|
|
});
|
|
});
|
|
|
|
test('User Merge - By Username with Name Override', async ({ page, api }) => {
|
|
await test.step('Configure SAML to identify users by username', async () => {
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
|
|
await expect((await setSettingValueById(api, 'SAML_Custom_Default_name_overwrite', true)).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser3');
|
|
|
|
await test.step('expect user data to have been mapped to the correct fields', async () => {
|
|
const user = await getUserInfo(api, 'user_for_saml_merge2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?._id).toBe('user_for_saml_merge2');
|
|
expect(user?.username).toBe('user_for_saml_merge2');
|
|
expect(user?.name).toBe('Saml User 3');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser3@example.com');
|
|
});
|
|
});
|
|
|
|
test('User Mapping - Custom Role', async ({ page, api }) => {
|
|
test.skip(!constants.IS_EE);
|
|
|
|
await doLoginStep(page, 'samluser4');
|
|
|
|
await test.step('expect users role to have been mapped correctly', async () => {
|
|
const user = await getUserInfo(api, 'samluser4');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('samluser4');
|
|
expect(user?.name).toBe('Saml User 4');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser4@example.com');
|
|
expect(user?.roles).toBeDefined();
|
|
expect(user?.roles?.length).toBe(1);
|
|
expect(user?.roles).toContain(samlRoleId);
|
|
});
|
|
});
|
|
|
|
test('Redirect to a specific group after login when using a valid invite link', async ({ page }) => {
|
|
await poRegistration.gotoInvite(inviteId);
|
|
await page.getByRole('link', { name: 'Back to Login' }).click();
|
|
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual(JSON.stringify(inviteId));
|
|
|
|
await doLoginStep(page, 'samluser1', null);
|
|
|
|
await test.step('expect to be redirected to the invited room after succesful login', async () => {
|
|
await expect(page).toHaveURL(`/group/${targetInviteGroupName}`);
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual('null');
|
|
});
|
|
});
|
|
|
|
test('Remove invite token from session storage if invite is not used', async ({ page }) => {
|
|
await poRegistration.gotoInvite(inviteId);
|
|
await page.getByRole('link', { name: 'Back to Login' }).click();
|
|
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual(JSON.stringify(inviteId));
|
|
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser2');
|
|
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual('null');
|
|
});
|
|
|
|
test('Redirect to home after login when no redirectUrl is provided', async ({ page }) => {
|
|
await doLoginStep(page, 'samluser2');
|
|
|
|
await test.step('expect to be redirected to the homepage after succesful login', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
});
|
|
});
|
|
|
|
test('Respect redirectUrl on multiple parallel logins', async ({ page, browser }) => {
|
|
const page2 = await browser.newPage();
|
|
const poRegistration2 = new Registration(page2);
|
|
|
|
await poRegistration2.goto();
|
|
await expect(page2).toHaveURL('/home');
|
|
|
|
await poRegistration.gotoInvite(inviteId);
|
|
await page.getByRole('link', { name: 'Back to Login' }).click();
|
|
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual(JSON.stringify(inviteId));
|
|
expect(await page2.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual('null');
|
|
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible();
|
|
await poRegistration.btnLoginWithSaml.click();
|
|
await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
|
|
await expect(page2.getByRole('button', { name: 'User menu' })).not.toBeVisible();
|
|
await expect(poRegistration2.btnLoginWithSaml).toBeVisible();
|
|
await poRegistration2.btnLoginWithSaml.click();
|
|
await expect(page2).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
|
|
|
|
await page.getByLabel('Username').fill('samluser1');
|
|
await page.getByLabel('Password').fill('password');
|
|
await page.locator('role=button[name="Login"]').click();
|
|
|
|
await page2.getByLabel('Username').fill('samluser2');
|
|
await page2.getByLabel('Password').fill('password');
|
|
await page2.locator('role=button[name="Login"]').click();
|
|
|
|
await expect(page).toHaveURL(`/group/${targetInviteGroupName}`);
|
|
await expect(page2).toHaveURL('/home');
|
|
|
|
expect(await page.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual('null');
|
|
expect(await page2.evaluate((key) => sessionStorage.getItem(key), KEY)).toEqual('null');
|
|
|
|
await page2.close();
|
|
});
|
|
|
|
test('Login - User without username can set initial username during first login', async ({ page, api }) => {
|
|
await doLoginStepWithUsernameSelection(page, 'samlusernoname2');
|
|
|
|
await test.step('expect to be redirected to the username selection page and set the username', async () => {
|
|
await expect(poRegistration.btnRegisterConfirmUsername).toBeVisible();
|
|
await doUsernameSelection('custom_saml_username2');
|
|
});
|
|
|
|
await test.step('expect to be redirected to the homepage after succesful login', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
|
|
});
|
|
|
|
await test.step('expect user data to have been created with custom username', async () => {
|
|
const user = await getUserInfo(api, 'custom_saml_username2');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('custom_saml_username2');
|
|
expect(user?.name).toBe('Saml User No Username 2');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samlusernoname2@example.com');
|
|
});
|
|
});
|
|
|
|
test.describe('Login - Username selection when allow username change is disabled', () => {
|
|
test.beforeAll(async ({ api }) => {
|
|
await expect((await setSettingValueById(api, 'Accounts_AllowUsernameChange', false)).status()).toBe(200);
|
|
});
|
|
|
|
test.afterAll(async ({ api }) => {
|
|
await expect((await setSettingValueById(api, 'Accounts_AllowUsernameChange', true)).status()).toBe(200);
|
|
});
|
|
|
|
test('User without username can set initial username when username changes are disabled', async ({ page, api }) => {
|
|
await doLoginStepWithUsernameSelection(page, 'samlusernoname');
|
|
|
|
await test.step('expect to be redirected to the username selection page and set the username', async () => {
|
|
await expect(poRegistration.btnRegisterConfirmUsername).toBeVisible();
|
|
await doUsernameSelection('custom_saml_username');
|
|
});
|
|
|
|
await test.step('expect to be redirected to the homepage after succesful login', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
|
|
});
|
|
|
|
await test.step('expect user data to have been created with the custom username', async () => {
|
|
const user = await getUserInfo(api, 'custom_saml_username');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('custom_saml_username');
|
|
expect(user?.name).toBe('Saml User No Username');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samlusernoname@example.com');
|
|
});
|
|
});
|
|
});
|
|
|
|
test.fixme('User Merge - By Custom Identifier', async () => {
|
|
// Test user merge with a custom identifier configured in the fieldmap
|
|
});
|
|
|
|
test.describe('Logout Request Signature Validation', () => {
|
|
// This is a valid logout request for user samluser1
|
|
const logoutRequest =
|
|
'http://localhost:3000/_saml/logout/test-sp/?SAMLRequest=lZJBi9swEIX%2FitE9sWRbTiIS00AoBLa7bVN66CWMpVHXYEuuNIb8%2FMpOF7YtXehNPM379GZG%2BwhDP6oH%2F91P9Bl%2FTBgpuw29i2q5ObApOOUhdlE5GDAq0upy%2FPCgijVXY%2FDkte%2FZK8vbDogRA3Xesex8OrBrKTd8V1UaWiNbu%2BPSbrmQtUCorN5IgwWgMHYDBcu%2BYojJeWAJlOwxTnh2kcBRknhRr7hc8eqLqBUvlRTfWHZK3XQOaHE9E40qz3uvoX%2F2kVTJOc%2Bvc%2BYkzv3nlOpXccxZ9ujpyT2FoyUMf9G3C73Zz1a15AjNW%2FQBCQwQvPD3%2BWvnHfOYRnU%2BZZeP8%2BHTBH1nu%2Fnp%2F%2BGy7L0PA9C%2FVyDWYlE6s7JLqcIBuv5oTMAYWTNjp7Qh8Q5vMIw9rrUffsW9J7zHHdUl1aexnp3BW3MtSmnLtkBbbrGuQYPQG2NbY2SlUeykgLLd6dYU8g77w%2F8i%2FvYNm58%3D&RelayState=_658ecfea419894f995dd3d5bc1dedc511ab252a0ad&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256';
|
|
const logoutRequestSignature =
|
|
'MM9fh6cesrjADYxkq9dr0aoyRxjG%2Fsi7lFyqjNex0F80sosmjZedwx%2FZC4iIr1DfK9ac7EuloXpp2sn%2BgLbLrsoo2kqY3AS5%2FQ6zQVpIdg%2BhAk5CcpGXmY%2B8CqvL2QsUnMv9nHuemXzgoSjwnvGeTlpBiuGCHFmcfFFBUVgq1sZO4JM43d5JUojhVI4nEAGPhbOiHFSvKB09pE1pOu6sDhSepb6sAbBIrRfZPW7DLcSGe1DoY1x6vRw%2F3hV88Z9WwZJmogJ69lhQOuv%2BbZwC%2BEWVIgre38GnbLkXOiifMUJnkxkHcCvku7ALhUD%2BPwPEaslFwjIb2eIN0PJUZcKJCg%3D%3D';
|
|
|
|
test.describe('Require Signature', () => {
|
|
test.beforeAll(async ({ api }) => {
|
|
await doChangeSetting(api, 'SAML_Custom_Default_validate_logout_request_signature', true);
|
|
});
|
|
|
|
test('Reject Invalid Signature on Logout Request', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await page.goto(`${logoutRequest}&Signature=invalid`);
|
|
|
|
await test.step('expect to be redirected back to rocket.chat without being logged out', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('Reject Missing Signature on Logout Request', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await page.goto(logoutRequest);
|
|
|
|
await test.step('expect to be redirected back to rocket.chat without being logged out', async () => {
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByRole('button', { name: 'User menu' })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('Accept Valid Signature on Logout Request', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await page.goto(`${logoutRequest}&Signature=${logoutRequestSignature}`);
|
|
|
|
await expectLoggedOutAfterLogoutRequest(page);
|
|
});
|
|
});
|
|
|
|
test.describe('Do not require Signature', () => {
|
|
test.beforeAll(async ({ api }) => {
|
|
await doChangeSetting(api, 'SAML_Custom_Default_validate_logout_request_signature', false);
|
|
});
|
|
|
|
test('Ignore Invalid Signature on Logout Request', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await page.goto(`${logoutRequest}&Signature=invalid`);
|
|
|
|
await expectLoggedOutAfterLogoutRequest(page);
|
|
});
|
|
|
|
test('Ignore Missing Signature on Logout Request', async ({ page }) => {
|
|
await poRegistration.goto();
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await page.goto(logoutRequest);
|
|
|
|
await expectLoggedOutAfterLogoutRequest(page);
|
|
});
|
|
});
|
|
});
|
|
|
|
test.fixme('Login - User without username', async () => {
|
|
// Test login with a SAML user with no username
|
|
// Test different variations of the Immutable Property setting
|
|
});
|
|
|
|
test.fixme('Login - User without email', async () => {
|
|
// Test login with a SAML user with no email
|
|
// Test different variations of the Immutable Property setting
|
|
});
|
|
|
|
test('Login - User without name', async ({ page, api }) => {
|
|
await test.step('expect user name to fallback to username when displayName is not provided', async () => {
|
|
await doLoginStep(page, 'samluser5');
|
|
|
|
const user = await getUserInfo(api, 'samluser5');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('samluser5');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser5@example.com');
|
|
// When displayName is not provided, it should fall back to username
|
|
expect(user?.name).toBe('samluser5');
|
|
await doLogoutStep(page);
|
|
});
|
|
|
|
await test.step('expect user name to fallback to displayName when provided', async () => {
|
|
await doLoginStep(page, 'samluser6');
|
|
|
|
const user = await getUserInfo(api, 'samluser6');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('samluser6');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser6@example.com');
|
|
// When displayName is provided, it should fall back to displayName
|
|
expect(user?.name).toBe('Saml User 6 Display Name');
|
|
});
|
|
});
|
|
|
|
test('Login - User with channels attribute', async ({ page, api }) => {
|
|
autoCreatedChannel = 'saml-channel-2';
|
|
|
|
await test.step('Configure SAML to enable channels attribute updates', async () => {
|
|
expect((await setSettingValueById(api, 'SAML_Custom_Default_channels_update', true)).status()).toBe(200);
|
|
});
|
|
|
|
await doLoginStep(page, 'samluser7');
|
|
|
|
await test.step('expect user data to have been mapped correctly', async () => {
|
|
const user = await getUserInfo(api, 'samluser7');
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.username).toBe('samluser7');
|
|
expect(user?.name).toBe('Saml User 7');
|
|
expect(user?.emails).toBeDefined();
|
|
expect(user?.emails?.[0].address).toBe('samluser7@example.com');
|
|
});
|
|
|
|
await test.step('expect auto-created channel to be created automatically', async () => {
|
|
const channelInfoResponse = await api.get(`/channels.info?roomName=${autoCreatedChannel}`);
|
|
expect(channelInfoResponse.status()).toBe(200);
|
|
const { channel } = await channelInfoResponse.json();
|
|
expect(channel).toBeDefined();
|
|
expect(channel.name).toBe(autoCreatedChannel);
|
|
});
|
|
|
|
await test.step('expect user to be member of existing channel', async () => {
|
|
const existingChannelMembersResponse = await api.get(`/channels.members?roomName=${targetChannel}`);
|
|
expect(existingChannelMembersResponse.status()).toBe(200);
|
|
const { members: existingMembers } = await existingChannelMembersResponse.json();
|
|
expect(existingMembers).toBeDefined();
|
|
expect(existingMembers.some((member: { username: string }) => member.username === 'samluser7')).toBe(true);
|
|
});
|
|
|
|
await test.step('expect user to be member of auto-created channel', async () => {
|
|
const autoCreatedChannelMembersResponse = await api.get(`/channels.members?roomName=${autoCreatedChannel}`);
|
|
expect(autoCreatedChannelMembersResponse.status()).toBe(200);
|
|
const { members: autoCreatedMembers } = await autoCreatedChannelMembersResponse.json();
|
|
expect(autoCreatedMembers).toBeDefined();
|
|
expect(autoCreatedMembers.some((member: { username: string }) => member.username === 'samluser7')).toBe(true);
|
|
});
|
|
|
|
await doLogoutStep(page);
|
|
|
|
await doLoginStep(page, 'samluser1');
|
|
|
|
await test.step('expect user without channels attribute to not be added to existing channel', async () => {
|
|
const existingChannelMembersResponse = await api.get(`/channels.members?roomName=${targetChannel}`);
|
|
expect(existingChannelMembersResponse.status()).toBe(200);
|
|
const { members: existingMembers } = await existingChannelMembersResponse.json();
|
|
expect(existingMembers).toBeDefined();
|
|
expect(existingMembers.some((member: { username: string }) => member.username === 'samluser1')).toBe(false);
|
|
});
|
|
|
|
await test.step('expect user without channels attribute to not be added to auto-created channel', async () => {
|
|
const autoCreatedChannelMembersResponse = await api.get(`/channels.members?roomName=${autoCreatedChannel}`);
|
|
expect(autoCreatedChannelMembersResponse.status()).toBe(200);
|
|
const { members: autoCreatedMembers } = await autoCreatedChannelMembersResponse.json();
|
|
expect(autoCreatedMembers).toBeDefined();
|
|
expect(autoCreatedMembers.some((member: { username: string }) => member.username === 'samluser1')).toBe(false);
|
|
});
|
|
});
|
|
|
|
test.describe('SAML Login handoff', () => {
|
|
// When a native client starts the login it passes `loginClient`, and the web client must hand the SAML
|
|
// credential token over to the app instead of logging itself in, so that only one session is ever created.
|
|
const findCredentialToken = async (credentialToken: string) => {
|
|
const connection = await MongoClient.connect(constants.URL_MONGODB);
|
|
try {
|
|
return await connection
|
|
.db()
|
|
.collection<{ _id: string; userInfo?: { profile?: Record<string, any> } }>('rocketchat_credential_tokens')
|
|
.findOne({ _id: credentialToken });
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
};
|
|
|
|
test.beforeAll(async ({ api }) => {
|
|
await api.post('/settings/Accounts_OAuth_Use_Modern_Flow', { value: true });
|
|
});
|
|
|
|
test.afterAll(async ({ api }) => {
|
|
await api.post('/settings/Accounts_OAuth_Use_Modern_Flow', { value: false });
|
|
});
|
|
|
|
test('Hand the credential token to the desktop client without logging in the browser', async ({ page }) => {
|
|
await poRegistration.goto('/home?loginClient=desktop');
|
|
|
|
await expect(page).toHaveURL(/loginClient=desktop/);
|
|
|
|
// Passing null skips the logged-in assertions, since the browser must not get a session here.
|
|
await doLoginStep(page, 'samluser1', null);
|
|
|
|
let credentialToken: string | null = null;
|
|
|
|
await test.step('expect to land on the SAML handoff route carrying the credential token', async () => {
|
|
// SAMLLoginRoute redirects to rocketchat://auth from here. The browser has no handler for that
|
|
// scheme, so the page stays put and we can inspect exactly what would have been handed over.
|
|
await expect(page).toHaveURL(/\/saml\/[^?]+\?.*loginClient=desktop/);
|
|
|
|
credentialToken = new URL(page.url()).searchParams.get('saml_idp_credentialToken');
|
|
expect(credentialToken).toBeTruthy();
|
|
});
|
|
|
|
await test.step('expect the credential to be stored server side, ready for the app to redeem', async () => {
|
|
const storedCredential = await findCredentialToken(credentialToken as string);
|
|
|
|
expect(storedCredential).not.toBeNull();
|
|
expect(storedCredential?.userInfo?.profile).toBeDefined();
|
|
expect(storedCredential?.userInfo?.profile?.email).toBe('samluser1@example.com');
|
|
});
|
|
|
|
await test.step('expect the browser to remain unauthenticated', async () => {
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible();
|
|
|
|
// Reload to prove no session was persisted for this browser.
|
|
await poRegistration.goto();
|
|
await expect(poRegistration.btnLoginWithSaml).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'User menu' })).not.toBeVisible();
|
|
});
|
|
});
|
|
});
|
|
|
|
test.fixme('Data Sync - Custom Field Map', async () => {
|
|
// Test the data sync using a custom fieldmap setting
|
|
});
|
|
});
|