mirror of
https://github.com/mattermost/mattermost.git
synced 2026-11-03 12:48:37 -06:00
[E2E/Playwright] Add password, MFA, authentication-method, and claim tests (#38520)
* Add Playwright password, MFA, authentication-method, and claim tests Adds password requirements, MFA visibility/enforcement, System Console authentication-method column, and authentication-transfer claim coverage. Deletes the ported Cypress specs. Locates the user row by exact username instead of assuming index 0, and waits for the users-report response matching the search term before reading it. Anchors the password-length error locator so it can't match the longer complexity message, and asserts the length reset to the compiled default (5). MM-T1778 skips when testcontainers isn't available, since its enforced-MFA cleanup depends on the local mmctl path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix Cypress override-setting test pollution and gh api --slurp/--jq CI script bug - helpers.js: make enableUsernameAndIconOverrideInt idempotent so a sibling spec leaving EnablePostUsernameOverride/EnablePostIconOverride already enabled doesn't leave #saveSetting permanently disabled (30s timeout in disallow_username_profile_override_spec.js). Replace the spec's dead mattermost.com icon fixture URL (404) with a stable in-repo asset. - e2e-tests-override-status.yml / e2e-tests-verified-label.yml: gh api rejects --slurp combined with --jq in one call; split into a pipe to a separate jq invocation instead. - e2e-tests-playwright.yml: bump playwright-full workers 20 -> 30. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5c970cdbb
commit
8bfa298a54
40 files changed
+1332
-473
No files matched your search
@@ -56,8 +56,8 @@ jobs:
|
||||
echo "Checking: $CONTEXT_NAME"
|
||||
|
||||
# Get current status
|
||||
STATUS_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${COMMIT_SHA}/statuses" --paginate --slurp \
|
||||
--jq "[.[][] | select(.context == \"$CONTEXT_NAME\")] | first // empty")
|
||||
STATUS_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${COMMIT_SHA}/statuses" --paginate --slurp | \
|
||||
jq "[.[][] | select(.context == \"$CONTEXT_NAME\")] | first // empty")
|
||||
|
||||
if [ -z "$STATUS_JSON" ]; then
|
||||
echo " No status found, skipping"
|
||||
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
pull-requests: write
|
||||
uses: ./.github/workflows/e2e-tests-playwright-template.yml
|
||||
with:
|
||||
workers: 20
|
||||
workers: 30
|
||||
enabled_docker_services: "postgres inbucket"
|
||||
commit_sha: ${{ inputs.commit_sha }}
|
||||
branch: ${{ needs.generate-build-variables.outputs.branch }}
|
||||
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
echo "Checking: $CONTEXT_NAME"
|
||||
|
||||
# Get current status
|
||||
STATUS_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${COMMIT_SHA}/statuses" --paginate --slurp \
|
||||
--jq "[.[][] | select(.context == \"$CONTEXT_NAME\")] | first // empty")
|
||||
STATUS_JSON=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${COMMIT_SHA}/statuses" --paginate --slurp | \
|
||||
jq "[.[][] | select(.context == \"$CONTEXT_NAME\")] | first // empty")
|
||||
|
||||
if [ -z "$STATUS_JSON" ]; then
|
||||
echo " No status found, skipping"
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Stage: @prod
|
||||
// Group: @channels @system_console @authentication @mfa
|
||||
|
||||
import * as TIMEOUTS from '@/fixtures/timeouts';
|
||||
import {getRandomId} from '@/utils';
|
||||
|
||||
describe('Authentication', () => {
|
||||
beforeEach(() => {
|
||||
// # Log in as admin.
|
||||
cy.apiAdminLogin();
|
||||
});
|
||||
|
||||
it('MM-T1771 - Minimum password length error field shows below minimum and above 72', () => {
|
||||
cy.visit('/admin_console/authentication/password');
|
||||
|
||||
cy.findByPlaceholderText('E.g.: "5"', {timeout: TIMEOUTS.ONE_MIN}).clear().type('88');
|
||||
|
||||
cy.uiSave();
|
||||
|
||||
// * Ensure error appears when saving a password outside of the limits
|
||||
// Note: minimum is 5 on non-FIPS builds and 14 on FIPS builds
|
||||
cy.contains(/Minimum password length must be a whole number greater than or equal to (5|14) and less than or equal to 72\./, {timeout: TIMEOUTS.ONE_MIN}).
|
||||
should('exist').
|
||||
and('be.visible');
|
||||
|
||||
cy.findByPlaceholderText('E.g.: "5"').clear().type('3');
|
||||
|
||||
cy.uiSave();
|
||||
|
||||
// * Ensure error appears when saving a password outside of the limits
|
||||
cy.contains(/Minimum password length must be a whole number greater than or equal to (5|14) and less than or equal to 72\./, {timeout: TIMEOUTS.ONE_MIN}).
|
||||
should('exist').
|
||||
and('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T1772 - Change minimum password length, verify help text and error message', () => {
|
||||
cy.visit('/admin_console/authentication/password');
|
||||
|
||||
cy.findByPlaceholderText('E.g.: "5"', {timeout: TIMEOUTS.ONE_MIN}).clear().type('15');
|
||||
|
||||
cy.uiSave();
|
||||
|
||||
cy.findByText('Your password must be 15-72 characters long.').should('be.visible');
|
||||
|
||||
cy.apiLogout();
|
||||
|
||||
// # Go to sign up with email page
|
||||
cy.visit('/signup_user_complete');
|
||||
|
||||
cy.get('#input_email', {timeout: TIMEOUTS.ONE_MIN}).type(`test-${getRandomId()}@example.com`);
|
||||
|
||||
cy.get('#input_name').clear().type(`BestUsername${getRandomId()}`);
|
||||
|
||||
cy.get('#input_password-input').clear().type('less');
|
||||
|
||||
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
|
||||
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert the error is what is expected;
|
||||
cy.findByText('Your password must be 15-72 characters long.').should('be.visible');
|
||||
|
||||
cy.get('#input_password-input').clear().type('GreaterThan15Chr!');
|
||||
|
||||
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
|
||||
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert that we are not shown an MFA screen and instead a Teams You Can join page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T1773 - Minimum password length field resets to default after saving invalid value', () => {
|
||||
cy.visit('/admin_console/authentication/password');
|
||||
|
||||
cy.findByPlaceholderText('E.g.: "5"', {timeout: TIMEOUTS.ONE_MIN}).clear().type('20');
|
||||
|
||||
cy.uiSave();
|
||||
|
||||
cy.reload();
|
||||
|
||||
// * Ensure the limit 20 appears
|
||||
cy.findByPlaceholderText('E.g.: "5"').invoke('val').should('equal', '20');
|
||||
cy.findByPlaceholderText('E.g.: "5"').clear();
|
||||
|
||||
cy.uiSave();
|
||||
|
||||
// Reload to see the actual server state, since on FIPS builds saving
|
||||
// the webapp default of 5 is rejected (below the FIPS minimum of 14).
|
||||
cy.reload();
|
||||
|
||||
// * Ensure the field reflects the server's current minimum password length
|
||||
cy.apiGetConfig().then(({config: {PasswordSettings}}) => {
|
||||
cy.findByPlaceholderText('E.g.: "5"').invoke('val').should('equal', String(PasswordSettings.MinimumLength));
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1774 - Select all Password Requirements, verify help text and error on bad password', () => {
|
||||
cy.apiUpdateConfig({
|
||||
PasswordSettings: {
|
||||
Lowercase: true,
|
||||
Number: true,
|
||||
Uppercase: true,
|
||||
Symbol: true,
|
||||
},
|
||||
});
|
||||
|
||||
cy.apiLogout();
|
||||
|
||||
// # Go to sign up with email page
|
||||
cy.visit('/signup_user_complete');
|
||||
|
||||
cy.get('#input_email', {timeout: TIMEOUTS.ONE_MIN}).type(`test-${getRandomId()}@example.com`);
|
||||
|
||||
cy.get('#input_name').clear().type(`BestUsernameInTheWorld${getRandomId()}`);
|
||||
|
||||
cy.get('#signup-body-card-form-check-terms-and-privacy').check();
|
||||
|
||||
['NOLOWERCASE12345!', 'nouppercase12345!', 'NoNumberHere!!!', 'NoSymbol1234567'].forEach((option) => {
|
||||
cy.get('#input_password-input').clear().type(option);
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert the error is what is expected;
|
||||
cy.findByText('Your password must be 14-72 characters long and include both lowercase and uppercase letters, numbers, and special characters.').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1777 - Multi-factor Authentication option hidden in Profile when disabled', () => {
|
||||
cy.apiUpdateConfig({
|
||||
ServiceSettings: {
|
||||
EnableMultifactorAuthentication: false,
|
||||
},
|
||||
});
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
// # Go to Profile
|
||||
cy.uiOpenProfileModal('Security');
|
||||
|
||||
// * Assert that Multi-factor Authentication text does not exist
|
||||
cy.findByText('Multi-factor Authentication').should('not.exist');
|
||||
});
|
||||
|
||||
it('MM-T1779 - Multi-factor Authentication option appears in Profile when enabled', () => {
|
||||
cy.apiUpdateConfig({
|
||||
ServiceSettings: {
|
||||
EnableMultifactorAuthentication: true,
|
||||
},
|
||||
});
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
// # Go to Profile
|
||||
cy.uiOpenProfileModal('Security');
|
||||
|
||||
// * Assert that Multi-factor Authentication text does exist
|
||||
cy.findByText('Multi-factor Authentication').should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T1780 - Multi-factor Authentication false: User can log in without being prompted for MFA', () => {
|
||||
cy.apiUpdateConfig({
|
||||
ServiceSettings: {
|
||||
EnableMultifactorAuthentication: true,
|
||||
EnforceMultifactorAuthentication: false,
|
||||
},
|
||||
});
|
||||
|
||||
cy.apiCreateUser().then(({user: newUser}) => {
|
||||
cy.apiLogout();
|
||||
|
||||
// # Login as a new user and visit default page
|
||||
cy.apiLogin(newUser);
|
||||
cy.visit('/');
|
||||
|
||||
// * Assert that we are not shown a MFA screen and instead a Teams You Can join page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
-155
@@ -1,155 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Group: @channels @enterprise @system_console @authentication @mfa
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
import * as authenticator from 'authenticator';
|
||||
|
||||
import * as TIMEOUTS from '@/fixtures/timeouts';
|
||||
|
||||
describe('Authentication', () => {
|
||||
let mfaSysAdmin: UserProfile;
|
||||
let testUser: UserProfile;
|
||||
let adminMFASecret: string;
|
||||
|
||||
before(() => {
|
||||
cy.apiRequireLicenseForFeature('MFA');
|
||||
|
||||
// # Do email test if setup properly
|
||||
cy.shouldHaveEmailEnabled();
|
||||
|
||||
cy.apiInitSetup().then(({user}) => {
|
||||
testUser = user;
|
||||
});
|
||||
|
||||
// # Create and login a newly created user as sysadmin
|
||||
cy.apiCreateCustomAdmin().then(({sysadmin}) => {
|
||||
mfaSysAdmin = sysadmin;
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1778 - MFA - Enforced', () => {
|
||||
// # Log in as a admin.
|
||||
cy.apiLogin(mfaSysAdmin);
|
||||
|
||||
// # Navigate to System Console -> Authentication -> MFA Page.
|
||||
cy.visit('/admin_console/authentication/mfa');
|
||||
cy.findByText('Multi-factor Authentication', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('exist');
|
||||
|
||||
// # Ensure the setting 'Enable Multi factor authentication' is set to true in the MFA page.
|
||||
cy.findByTestId('ServiceSettings.EnableMultifactorAuthenticationtrue').check();
|
||||
|
||||
// # Also ensure that this MFA setting is enforced.
|
||||
cy.findByTestId('ServiceSettings.EnforceMultifactorAuthenticationtrue').check();
|
||||
|
||||
// # Click "Save".
|
||||
cy.findByText('Save').click().wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
// # Get MFA secret
|
||||
cy.uiGetMFASecret(mfaSysAdmin.id).then((secret) => {
|
||||
adminMFASecret = secret;
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option not available for the user
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('not.exist');
|
||||
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
// # Login as test user
|
||||
cy.uiLogin(testUser);
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// * MFA page is shown
|
||||
cy.findByText('Multi-factor Authentication Setup', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('exist');
|
||||
});
|
||||
|
||||
// This test relies on the previous test for having MFA enabled (MM-T1778)
|
||||
it('MM-T1781 - MFA - Admin removes another users MFA', () => {
|
||||
// # Login as test user
|
||||
cy.apiLogin(testUser);
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// # Complete MFA setup which we didnt do for the test user
|
||||
cy.get('#mfa').wait(TIMEOUTS.HALF_SEC).find('.col-sm-12').then((p) => {
|
||||
const secretp = p.text();
|
||||
const testUserMFASecret = secretp.split(' ')[1];
|
||||
|
||||
const token = authenticator.generateToken(testUserMFASecret);
|
||||
cy.findByPlaceholderText('MFA Code').type(token);
|
||||
cy.findByText('Save').click();
|
||||
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Okay').click();
|
||||
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// # Login back as admin.
|
||||
const adminMFAToken = authenticator.generateToken(adminMFASecret);
|
||||
cy.apiLoginWithMFA(mfaSysAdmin, adminMFAToken);
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option available for the user and click it
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('be.visible').click();
|
||||
|
||||
// # Navigate to System Console -> Authentication -> MFA Page.
|
||||
cy.visit('/admin_console/authentication/mfa');
|
||||
cy.findByText('Multi-factor Authentication', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('exist');
|
||||
|
||||
// # Also ensure that this MFA setting is enforced.
|
||||
cy.findByTestId('ServiceSettings.EnforceMultifactorAuthenticationfalse').check();
|
||||
|
||||
// # Click "Save".
|
||||
cy.findByText('Save').click().wait(TIMEOUTS.ONE_SEC);
|
||||
cy.apiLogout();
|
||||
|
||||
// # Login as test user
|
||||
cy.apiLogin(testUser);
|
||||
cy.visit('/');
|
||||
|
||||
// * No MFA page is shown
|
||||
cy.findByText('Multi-factor Authentication Setup', {timeout: TIMEOUTS.ONE_MIN}).should('not.exist').and('not.exist');
|
||||
});
|
||||
|
||||
// This test relies on the previous test for having MFA enabled (MM-T1781)
|
||||
it('MM-T1782 - MFA - Removing MFA option hidden for users without MFA set up', () => {
|
||||
// # Login back as admin.
|
||||
const token = authenticator.generateToken(adminMFASecret);
|
||||
cy.apiLoginWithMFA(mfaSysAdmin, token);
|
||||
|
||||
// # Navigate to System Console -> User Management -> Users
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
cy.findByPlaceholderText('Search users').type(testUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Remove MFA option available for the user and click it
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByText('Remove MFA').should('not.exist');
|
||||
|
||||
// # Done with that MFA stuff so we disable it all
|
||||
cy.apiUpdateConfig({
|
||||
ServiceSettings: {
|
||||
EnableMultifactorAuthentication: false,
|
||||
EnforceMultifactorAuthentication: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***************************************************************
|
||||
// - [#] indicates a test step (e.g. # Go to a page)
|
||||
// - [*] indicates an assertion (e.g. * Check the title)
|
||||
// - Use element ID when selecting an element. Create one if none.
|
||||
// ***************************************************************
|
||||
|
||||
// Group: @channels @enterprise @system_console @mfa
|
||||
|
||||
import ldapUsers from '@/fixtures/ldap_users.json';
|
||||
import * as TIMEOUTS from '@/fixtures/timeouts';
|
||||
import {getAdminAccount} from '@/support/env';
|
||||
|
||||
const authenticator = require('authenticator');
|
||||
|
||||
describe('Settings', () => {
|
||||
let mfaUser;
|
||||
let samlUser;
|
||||
|
||||
const ldapUser = ldapUsers['test-1'];
|
||||
|
||||
before(() => {
|
||||
cy.apiInitSetup().then(({user}) => {
|
||||
mfaUser = user;
|
||||
|
||||
cy.apiUpdateConfig({
|
||||
ServiceSettings: {
|
||||
EnableMultifactorAuthentication: true,
|
||||
},
|
||||
});
|
||||
|
||||
// * Check if server has license for LDAP
|
||||
cy.apiRequireLicenseForFeature('LDAP');
|
||||
|
||||
return cy.apiSyncLDAPUser({ldapUser});
|
||||
}).then(() => {
|
||||
return cy.apiCreateUser();
|
||||
}).then(({user: user2}) => {
|
||||
// # Create SAML user
|
||||
samlUser = user2;
|
||||
const body = {
|
||||
from: 'email',
|
||||
auto: false,
|
||||
};
|
||||
body.matches = {};
|
||||
body.matches[user2.email] = user2.username;
|
||||
|
||||
return migrateAuthToSAML(body);
|
||||
}).then(() => {
|
||||
return cy.apiGenerateMfaSecret(mfaUser.id);
|
||||
}).then((res) => {
|
||||
// # Create MFA user
|
||||
const token = authenticator.generateToken(res.code.secret);
|
||||
|
||||
return cy.apiActivateUserMFA(mfaUser.id, true, token);
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T953 Verify correct authentication method', () => {
|
||||
cy.visit('/admin_console/user_management/users');
|
||||
|
||||
const adminUsername = getAdminAccount().username;
|
||||
|
||||
// # Type sysadmin
|
||||
cy.findByPlaceholderText('Search users').clear().type(adminUsername).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'Email');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type saml user
|
||||
cy.findByPlaceholderText('Search users').clear().type(samlUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'SAML');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type ldap user
|
||||
cy.findByPlaceholderText('Search users').clear().type(ldapUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('have.text', 'LDAP');
|
||||
cy.get('a.back').click();
|
||||
|
||||
// # Type mfa user
|
||||
cy.findByPlaceholderText('Search users').clear().type(mfaUser.username).wait(TIMEOUTS.TWO_SEC);
|
||||
|
||||
// * Verify sign-in method from user profile
|
||||
cy.get('.displayName').click();
|
||||
cy.get('label').contains('Authentication Method').find('span').last().should('contain.text', 'MFA');
|
||||
cy.get('a.back').click();
|
||||
});
|
||||
});
|
||||
|
||||
function migrateAuthToSAML(body) {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: '/api/v4/users/migrate_auth/saml',
|
||||
method: 'POST',
|
||||
body,
|
||||
timeout: TIMEOUTS.ONE_MIN,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
+4
-1
@@ -45,7 +45,10 @@ describe('Incoming webhook', () => {
|
||||
});
|
||||
|
||||
it('MM-T622 Disallow override of username and profile picture', () => {
|
||||
const iconUrl = 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png';
|
||||
// The mattermost.com marketing asset this used to point at was removed (404), which the
|
||||
// server can't fetch/proxy — points at a fixture already committed to this repo instead,
|
||||
// so the URL doesn't rot again.
|
||||
const iconUrl = 'https://raw.githubusercontent.com/mattermost/mattermost/master/e2e-tests/cypress/tests/fixtures/mattermost-icon.png';
|
||||
|
||||
// # Enable username and icon override
|
||||
cy.apiAdminLogin();
|
||||
|
||||
+22
-5
@@ -14,10 +14,27 @@ export function enableUsernameAndIconOverride(enable) {
|
||||
export function enableUsernameAndIconOverrideInt(enableUsername, enableIcon) {
|
||||
// # Visit integration management at system console and change override values
|
||||
cy.visit('/admin_console/integrations/integration_management');
|
||||
cy.findByTestId('ServiceSettings.EnablePostUsernameOverride' + enableUsername).check({force: true});
|
||||
cy.findByTestId('ServiceSettings.EnablePostIconOverride' + enableIcon).check({force: true});
|
||||
|
||||
// # Save the settings
|
||||
cy.get('#saveSetting').should('be.enabled').click({force: true});
|
||||
cy.get('#saveSetting').should('be.disabled');
|
||||
// Each option is a radio pair (testid suffixed 'true'/'false'). Only click the ones not
|
||||
// already in the desired state: a sibling spec can leave this setting already matching,
|
||||
// and clicking an already-checked radio doesn't mark the form dirty, so unconditionally
|
||||
// waiting for Save to become enabled would time out.
|
||||
checkIfNotAlreadyChecked('ServiceSettings.EnablePostUsernameOverride' + enableUsername);
|
||||
checkIfNotAlreadyChecked('ServiceSettings.EnablePostIconOverride' + enableIcon);
|
||||
|
||||
// # Save the settings only if something actually changed
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('#saveSetting:not(:disabled)').length > 0) {
|
||||
cy.get('#saveSetting').click({force: true});
|
||||
cy.get('#saveSetting').should('be.disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkIfNotAlreadyChecked(testId) {
|
||||
cy.findByTestId(testId).then(($el) => {
|
||||
if (!$el.prop('checked')) {
|
||||
cy.wrap($el).check({force: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -95,6 +95,8 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
|
||||
EnableOnboardingFlow: false,
|
||||
EnableSecurityFixAlert: false,
|
||||
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
|
||||
EnableMultifactorAuthentication: false,
|
||||
EnforceMultifactorAuthentication: false,
|
||||
},
|
||||
TeamSettings: {
|
||||
EnableOpenServer: true,
|
||||
|
||||
@@ -79,7 +79,7 @@ export {ensureLocalFile, listMattermostDataFiles} from './filestore';
|
||||
export {ensurePostgresSearch} from './postgres_search';
|
||||
export {ensureFeatureFlag} from './feature_flags';
|
||||
export {ensureServerEnv, ensureSiteUrl} from './server_env';
|
||||
export {generateTotp, enableUserMfa} from './mfa';
|
||||
export {generateTotp, enableUserMfa, disableMfa} from './mfa';
|
||||
export {runMmctl, runMmctlLocal, ensureMmctl} from './mmctl';
|
||||
export type {MmctlResult} from './mmctl';
|
||||
export {upgradeServerImage} from './version';
|
||||
|
||||
@@ -5,6 +5,10 @@ import {createHmac} from 'node:crypto';
|
||||
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {runMmctlLocal} from './mmctl';
|
||||
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
function base32Decode(secret: string): Buffer {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
const cleaned = secret.toUpperCase().replace(/[=+\s]/g, '');
|
||||
@@ -44,3 +48,40 @@ export async function enableUserMfa(adminClient: Client4, userId: string) {
|
||||
const {secret} = await adminClient.generateMfaSecret(userId);
|
||||
await adminClient.updateUserMfa(userId, true, generateTotp(secret));
|
||||
}
|
||||
|
||||
/** Turns MFA off without an admin API session, since enforced MFA can lock unenrolled admins out of patchConfig. */
|
||||
export async function disableMfa(adminClient?: Client4): Promise<void> {
|
||||
if (testConfig.mattermostContainerId) {
|
||||
const enable = await runMmctlLocal([
|
||||
'config',
|
||||
'set',
|
||||
'ServiceSettings.EnableMultifactorAuthentication',
|
||||
'false',
|
||||
]);
|
||||
const enforce = await runMmctlLocal([
|
||||
'config',
|
||||
'set',
|
||||
'ServiceSettings.EnforceMultifactorAuthentication',
|
||||
'false',
|
||||
]);
|
||||
if (enable.exitCode !== 0 || enforce.exitCode !== 0) {
|
||||
throw new Error(`Failed to disable MFA: ${enable.output} ${enforce.output}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!adminClient) {
|
||||
throw new Error('disableMfa requires a Mattermost container or an admin client.');
|
||||
}
|
||||
|
||||
try {
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableMultifactorAuthentication: false, EnforceMultifactorAuthentication: false},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'disableMfa could not restore MFA via the admin API. Enforcing MFA invalidates unenrolled admin sessions, so patchConfig returns 403. Run against Testcontainers Mattermost so mmctl --local can disable MFA, or enroll the admin in MFA before enforcing it. Original error: ' +
|
||||
String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
createUserWithAttributes,
|
||||
deleteKeycloakUser,
|
||||
deleteLdapUser,
|
||||
disableMfa,
|
||||
elasticsearchServerConfig,
|
||||
enableAIBridgeTestMode,
|
||||
ensureAzurite,
|
||||
@@ -167,6 +168,7 @@ export class PlaywrightExtended {
|
||||
readonly ensureOpenldap;
|
||||
readonly ensureOpensearch;
|
||||
readonly ensurePostgresSearch;
|
||||
readonly disableMfa;
|
||||
readonly enableUserMfa;
|
||||
readonly ensureServerEnv;
|
||||
readonly ensureSiteUrl;
|
||||
@@ -231,8 +233,13 @@ export class PlaywrightExtended {
|
||||
readonly signupPage;
|
||||
readonly selectTeamPage;
|
||||
readonly verifyEmailPage;
|
||||
readonly mfaSetupPage;
|
||||
readonly resetPasswordPage;
|
||||
readonly errorPage;
|
||||
readonly emailToOAuthPage;
|
||||
readonly oauthToEmailPage;
|
||||
readonly emailToLdapPage;
|
||||
readonly ldapToEmailPage;
|
||||
|
||||
// Same default page as above, post-login, for specs that authenticate it directly.
|
||||
readonly channelsPage;
|
||||
@@ -288,6 +295,7 @@ export class PlaywrightExtended {
|
||||
this.ensureOpenldap = ensureOpenldap;
|
||||
this.ensureOpensearch = ensureOpensearch;
|
||||
this.ensurePostgresSearch = ensurePostgresSearch;
|
||||
this.disableMfa = disableMfa;
|
||||
this.enableUserMfa = enableUserMfa;
|
||||
this.ensureServerEnv = ensureServerEnv;
|
||||
this.ensureSiteUrl = ensureSiteUrl;
|
||||
@@ -324,8 +332,13 @@ export class PlaywrightExtended {
|
||||
this.signupPage = new pages.SignupPage(page);
|
||||
this.selectTeamPage = new pages.SelectTeamPage(page);
|
||||
this.verifyEmailPage = new pages.VerifyEmailPage(page);
|
||||
this.mfaSetupPage = new pages.MfaSetupPage(page);
|
||||
this.resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
this.errorPage = new pages.ErrorPage(page);
|
||||
this.emailToOAuthPage = new pages.EmailToOAuthPage(page);
|
||||
this.oauthToEmailPage = new pages.OAuthToEmailPage(page);
|
||||
this.emailToLdapPage = new pages.EmailToLdapPage(page);
|
||||
this.ldapToEmailPage = new pages.LdapToEmailPage(page);
|
||||
|
||||
// Same default page as above, post-login
|
||||
this.channelsPage = new pages.ChannelsPage(page);
|
||||
|
||||
@@ -165,12 +165,45 @@ class ProfileSettingsTab {
|
||||
|
||||
class SecurityTab {
|
||||
readonly container: Locator;
|
||||
readonly mfaHeading: Locator;
|
||||
readonly signInHeading: Locator;
|
||||
readonly switchToOpenId: Locator;
|
||||
readonly switchToSaml: Locator;
|
||||
readonly switchToLdap: Locator;
|
||||
readonly switchToEmail: Locator;
|
||||
readonly editSignInMethod: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.mfaHeading = container.getByText('Multi-factor Authentication', {exact: true});
|
||||
this.signInHeading = container.getByText('Sign-in Method', {exact: true});
|
||||
this.editSignInMethod = container.locator('#signinEdit');
|
||||
this.switchToOpenId = container.getByRole('link', {name: 'Switch to Using OpenID SSO'});
|
||||
this.switchToSaml = container.getByRole('link', {name: 'Switch to Using SAML SSO'});
|
||||
this.switchToLdap = container.getByRole('link', {name: 'Switch to Using AD/LDAP'});
|
||||
this.switchToEmail = container.getByRole('link', {name: 'Switch to Using Email and Password'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async openSignInMethod() {
|
||||
await this.editSignInMethod.click();
|
||||
}
|
||||
|
||||
async clickSwitchToOpenId() {
|
||||
await this.openSignInMethod();
|
||||
await this.switchToOpenId.click();
|
||||
}
|
||||
|
||||
async clickSwitchToLdap() {
|
||||
await this.openSignInMethod();
|
||||
await this.switchToLdap.click();
|
||||
}
|
||||
|
||||
async clickSwitchToEmail() {
|
||||
await this.openSignInMethod();
|
||||
await this.switchToEmail.click();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Locator} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* System Console -> Authentication -> MFA
|
||||
*/
|
||||
export default class MfaSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly header: Locator;
|
||||
readonly enableTrue: Locator;
|
||||
readonly enableFalse: Locator;
|
||||
readonly enforceTrue: Locator;
|
||||
readonly enforceFalse: Locator;
|
||||
readonly saveButton: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.header = container.getByText('Multi-factor Authentication', {exact: true});
|
||||
this.enableTrue = container.getByTestId('ServiceSettings.EnableMultifactorAuthenticationtrue');
|
||||
this.enableFalse = container.getByTestId('ServiceSettings.EnableMultifactorAuthenticationfalse');
|
||||
this.enforceTrue = container.getByTestId('ServiceSettings.EnforceMultifactorAuthenticationtrue');
|
||||
this.enforceFalse = container.getByTestId('ServiceSettings.EnforceMultifactorAuthenticationfalse');
|
||||
this.saveButton = container.getByRole('button', {name: 'Save'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.header).toBeVisible();
|
||||
}
|
||||
|
||||
async save() {
|
||||
await expect(this.saveButton).toBeEnabled();
|
||||
await this.saveButton.click();
|
||||
await expect(this.saveButton).toBeDisabled();
|
||||
}
|
||||
}
|
||||
+5
@@ -44,4 +44,9 @@ export default class PasswordSettings {
|
||||
await expect(this.saveButton).toBeEnabled();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await this.container.page().reload();
|
||||
await this.toBeVisible();
|
||||
}
|
||||
}
|
||||
+4
@@ -169,6 +169,10 @@ class AdminUserCard {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async expectAuthenticationMethod(text: string | RegExp) {
|
||||
await expect(this.authenticationMethod).toHaveText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field-column container for a field by its exact label text.
|
||||
*/
|
||||
|
||||
+7
-1
@@ -118,13 +118,19 @@ export default class Users {
|
||||
* Search for users by typing in the search input
|
||||
*/
|
||||
async searchUsers(searchTerm: string) {
|
||||
const responsePromise = waitForUsersReportResponse(this.page);
|
||||
const responsePromise = waitForUsersReportResponse(this.page, searchTerm);
|
||||
await this.searchInput.fill(searchTerm);
|
||||
// SystemUsersSearch debounces dispatch by 500ms before fetching.
|
||||
await responsePromise;
|
||||
await expect(this.usersTable.getColumnHeader('User details')).toBeEnabled();
|
||||
}
|
||||
|
||||
async openUserActions(searchTerm: string) {
|
||||
await this.searchUsers(searchTerm);
|
||||
const row = await this.usersTable.getRowByUsername(searchTerm);
|
||||
return row.openActionMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search input
|
||||
*/
|
||||
|
||||
+8
@@ -60,4 +60,12 @@ export class UserActionMenu {
|
||||
async clickRevokeSessions() {
|
||||
await this.clickMenuItem('Revoke sessions');
|
||||
}
|
||||
|
||||
async clickRemoveMfa() {
|
||||
await this.clickMenuItem('Remove MFA');
|
||||
}
|
||||
|
||||
async expectRemoveMfaHidden() {
|
||||
await expect(this.getMenuItem('Remove MFA')).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
+31
-6
@@ -13,13 +13,22 @@ const USERS_REPORT_RESPONSE_TIMEOUT_MS = 10_000;
|
||||
* action that triggers the fetch (click, search, etc.) so Playwright does not sit
|
||||
* on the timeout when the response already finished.
|
||||
*/
|
||||
export function waitForUsersReportResponse(page: Page) {
|
||||
export function waitForUsersReportResponse(page: Page, searchTerm?: string) {
|
||||
return page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/api/v4/reports/users') &&
|
||||
!response.url().includes('/count') &&
|
||||
response.request().method() === 'GET' &&
|
||||
response.ok(),
|
||||
(response) => {
|
||||
if (
|
||||
!response.url().includes('/api/v4/reports/users') ||
|
||||
response.url().includes('/count') ||
|
||||
response.request().method() !== 'GET' ||
|
||||
!response.ok()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (searchTerm === undefined) {
|
||||
return true;
|
||||
}
|
||||
return new URL(response.url()).searchParams.get('search_term') === searchTerm;
|
||||
},
|
||||
{timeout: USERS_REPORT_RESPONSE_TIMEOUT_MS},
|
||||
);
|
||||
}
|
||||
@@ -80,6 +89,22 @@ export class UsersTable {
|
||||
return new UserRow(this.bodyRows.nth(index), index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user row by exact username, since the search is a partial LIKE match and can
|
||||
* return more than one row.
|
||||
*/
|
||||
async getRowByUsername(username: string): Promise<UserRow> {
|
||||
const count = await this.bodyRows.count();
|
||||
for (let index = 0; index < count; index++) {
|
||||
const row = this.getRowByIndex(index);
|
||||
const text = (await row.userName.textContent())?.trim();
|
||||
if (text === username) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
throw new Error(`No user row found for username "${username}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a column header by display name
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class EmailToLdapPage {
|
||||
readonly page: Page;
|
||||
readonly title;
|
||||
readonly emailPasswordInput;
|
||||
readonly ldapIdInput;
|
||||
readonly ldapPasswordInput;
|
||||
readonly submitButton;
|
||||
readonly invalidPasswordError;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.getByRole('heading', {name: 'Switch Email/Password Account to AD/LDAP'});
|
||||
this.emailPasswordInput = page.locator('#claim input[name="emailPassword"]');
|
||||
this.ldapIdInput = page.locator('#claim input[name="ldapId"]');
|
||||
this.ldapPasswordInput = page.locator('#claim input[name="ldapPassword"]');
|
||||
this.submitButton = page.getByRole('button', {name: 'Switch Account to AD/LDAP'});
|
||||
this.invalidPasswordError = page.getByText('Login failed because of invalid password.');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.emailPasswordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async submit(emailPassword: string, ldapId: string, ldapPassword: string) {
|
||||
await this.emailPasswordInput.fill(emailPassword);
|
||||
await this.ldapIdInput.fill(ldapId);
|
||||
await this.ldapPasswordInput.fill(ldapPassword);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class EmailToOAuthPage {
|
||||
readonly page: Page;
|
||||
readonly title;
|
||||
readonly passwordInput;
|
||||
readonly submitButton;
|
||||
readonly invalidPasswordError;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.getByRole('heading', {name: /Switch Email\/Password Account to .+ SSO/});
|
||||
this.passwordInput = page.locator('#claim input[name="password"]');
|
||||
this.submitButton = page.getByRole('button', {name: /Switch Account to .+ SSO/});
|
||||
this.invalidPasswordError = page.getByText('Login failed because of invalid password.');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async submit(password: string) {
|
||||
await this.passwordInput.fill(password);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ import ResetPasswordPage from './reset_password';
|
||||
import SignupPage from './signup';
|
||||
import SelectTeamPage from './select_team';
|
||||
import VerifyEmailPage from './verify_email';
|
||||
import MfaSetupPage from './mfa_setup';
|
||||
import EmailToOAuthPage from './email_to_oauth';
|
||||
import OAuthToEmailPage from './oauth_to_email';
|
||||
import EmailToLdapPage from './email_to_ldap';
|
||||
import LdapToEmailPage from './ldap_to_email';
|
||||
import SystemConsolePage from './system_console';
|
||||
import ScheduledPostsPage from './scheduled_posts';
|
||||
import DraftsPage from './drafts';
|
||||
@@ -27,6 +32,11 @@ const pages = {
|
||||
SignupPage,
|
||||
SelectTeamPage,
|
||||
VerifyEmailPage,
|
||||
MfaSetupPage,
|
||||
EmailToOAuthPage,
|
||||
OAuthToEmailPage,
|
||||
EmailToLdapPage,
|
||||
LdapToEmailPage,
|
||||
ScheduledPostsPage,
|
||||
ContentReviewPage,
|
||||
SystemConsolePage,
|
||||
@@ -49,6 +59,11 @@ export {
|
||||
SignupPage,
|
||||
SelectTeamPage,
|
||||
VerifyEmailPage,
|
||||
MfaSetupPage,
|
||||
EmailToOAuthPage,
|
||||
OAuthToEmailPage,
|
||||
EmailToLdapPage,
|
||||
LdapToEmailPage,
|
||||
ScheduledPostsPage,
|
||||
SystemConsolePage,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class LdapToEmailPage {
|
||||
readonly page: Page;
|
||||
readonly title;
|
||||
readonly ldapPasswordInput;
|
||||
readonly passwordInput;
|
||||
readonly confirmPasswordInput;
|
||||
readonly submitButton;
|
||||
readonly mfaTokenInput;
|
||||
readonly mfaSubmitButton;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.getByRole('heading', {name: 'Switch AD/LDAP Account to Email/Password'});
|
||||
this.ldapPasswordInput = page.locator('#claim input[name="ldapPassword"]');
|
||||
this.passwordInput = page.locator('#claim input[name="password"]');
|
||||
this.confirmPasswordInput = page.locator('#claim input[name="passwordconfirm"]');
|
||||
this.submitButton = page.getByRole('button', {name: 'Switch account to email/password'});
|
||||
this.mfaTokenInput = page.getByRole('textbox', {name: 'MFA Token'});
|
||||
this.mfaSubmitButton = page.getByRole('button', {name: 'Submit'});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.ldapPasswordInput.or(this.mfaTokenInput)).toBeVisible();
|
||||
|
||||
// The page currently opens on the MFA interstitial; a dummy token reveals the form.
|
||||
if (await this.mfaTokenInput.isVisible()) {
|
||||
await this.mfaTokenInput.fill('000000');
|
||||
await this.mfaSubmitButton.click();
|
||||
await expect(this.ldapPasswordInput).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
async submit(ldapPassword: string, newPassword: string) {
|
||||
await this.ldapPasswordInput.fill(ldapPassword);
|
||||
await this.passwordInput.fill(newPassword);
|
||||
await this.confirmPasswordInput.fill(newPassword);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class MfaSetupPage {
|
||||
readonly page: Page;
|
||||
readonly title: ReturnType<Page['getByText']>;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.getByText('Multi-factor Authentication Setup', {exact: true});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.title).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeHidden() {
|
||||
await expect(this.title).toBeHidden();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export default class OAuthToEmailPage {
|
||||
readonly page: Page;
|
||||
readonly title;
|
||||
readonly passwordInput;
|
||||
readonly confirmPasswordInput;
|
||||
readonly submitButton;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.getByRole('heading', {name: /Switch .+ Account to Email/});
|
||||
this.passwordInput = page.locator('#claim input[name="password"]');
|
||||
this.confirmPasswordInput = page.locator('#claim input[name="passwordconfirm"]');
|
||||
this.submitButton = page.getByRole('button', {name: /Switch .+ to Email and Password/});
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async submit(password: string) {
|
||||
await this.passwordInput.fill(password);
|
||||
await this.confirmPasswordInput.fill(password);
|
||||
await this.submitButton.click();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ export default class SignupPage {
|
||||
readonly emailError;
|
||||
readonly usernameError;
|
||||
readonly passwordError;
|
||||
readonly passwordLengthError;
|
||||
readonly passwordComplexityError;
|
||||
readonly domainRestrictionError;
|
||||
readonly noSignInMethods;
|
||||
readonly createAccountWithFollowing;
|
||||
@@ -54,6 +56,10 @@ export default class SignupPage {
|
||||
'Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.',
|
||||
);
|
||||
this.passwordError = page.getByText(/Must be \d+-72 characters long\./);
|
||||
this.passwordLengthError = page.getByText(/Your password must be \d+-72 characters long\.$/);
|
||||
this.passwordComplexityError = page.getByText(
|
||||
/Your password must be \d+-72 characters long and include both lowercase and uppercase letters, numbers, and special characters\./,
|
||||
);
|
||||
this.domainRestrictionError = page.getByText(
|
||||
'The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email.',
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import PluginManagement from '@/ui/components/system_console/sections/plugins/pl
|
||||
import OpenIdConnect from '@/ui/components/system_console/sections/authentication/openid_connect';
|
||||
import AdLdap from '@/ui/components/system_console/sections/authentication/ad_ldap';
|
||||
import PasswordSettings from '@/ui/components/system_console/sections/authentication/password';
|
||||
import MfaSettings from '@/ui/components/system_console/sections/authentication/mfa';
|
||||
import PublicLinks from '@/ui/components/system_console/sections/site_configuration/public_links';
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
@@ -72,6 +73,7 @@ export default class SystemConsolePage {
|
||||
readonly openIdConnect: OpenIdConnect;
|
||||
readonly adLdap: AdLdap;
|
||||
readonly passwordSettings: PasswordSettings;
|
||||
readonly mfaSettings: MfaSettings;
|
||||
readonly publicLinks: PublicLinks;
|
||||
|
||||
// Same page after logging out of the System Console
|
||||
@@ -122,6 +124,7 @@ export default class SystemConsolePage {
|
||||
this.openIdConnect = new OpenIdConnect(adminConsoleWrapper);
|
||||
this.adLdap = new AdLdap(adminConsoleWrapper);
|
||||
this.passwordSettings = new PasswordSettings(adminConsoleWrapper);
|
||||
this.mfaSettings = new MfaSettings(adminConsoleWrapper);
|
||||
this.publicLinks = new PublicLinks(adminConsoleWrapper);
|
||||
|
||||
this.loginPage = new LoginPage(page);
|
||||
@@ -162,6 +165,11 @@ export default class SystemConsolePage {
|
||||
await this.users.userDetail.toBeVisible();
|
||||
}
|
||||
|
||||
async gotoUsers() {
|
||||
await this.page.goto(new URL('/admin_console/user_management/users', testConfig.baseURL).href);
|
||||
await this.users.toBeVisible();
|
||||
}
|
||||
|
||||
async gotoAdLdap() {
|
||||
await this.page.goto(new URL('/admin_console/authentication/ldap', testConfig.baseURL).href);
|
||||
await this.adLdap.toBeVisible();
|
||||
@@ -172,6 +180,11 @@ export default class SystemConsolePage {
|
||||
await this.passwordSettings.toBeVisible();
|
||||
}
|
||||
|
||||
async gotoMfaSettings() {
|
||||
await this.page.goto(new URL('/admin_console/authentication/mfa', testConfig.baseURL).href);
|
||||
await this.mfaSettings.toBeVisible();
|
||||
}
|
||||
|
||||
async gotoPublicLinks() {
|
||||
await this.page.goto(new URL('/admin_console/site_config/public_links', testConfig.baseURL).href);
|
||||
await this.publicLinks.toBeVisible();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an email/password user can switch their sign-in method to AD/LDAP.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with OpenLDAP configured and authentication transfer enabled.
|
||||
*/
|
||||
test('switches an email account to AD/LDAP', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureOpenldap();
|
||||
const ldapUser = {
|
||||
username: `ldap${pw.random.id()}`,
|
||||
password: 'Password1',
|
||||
email: user.email,
|
||||
firstname: user.first_name || 'First',
|
||||
lastname: user.last_name || 'Last',
|
||||
};
|
||||
|
||||
try {
|
||||
await pw.createLdapUser(ldapUser);
|
||||
|
||||
// # Log in as the email user and open Switch to AD/LDAP
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToLdap();
|
||||
await pw.emailToLdapPage.toBeVisible();
|
||||
|
||||
// # Confirm the email password and the matching LDAP credentials
|
||||
await pw.emailToLdapPage.submit(user.password, ldapUser.username, ldapUser.password);
|
||||
|
||||
// * Verify the account now authenticates through LDAP
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
const switched = await adminClient.getUser(user.id);
|
||||
expect(switched.auth_service).toBe('ldap');
|
||||
|
||||
// # Log in with the LDAP username and password
|
||||
await pw.loginPage.submitCredentials(ldapUser.username, ldapUser.password);
|
||||
|
||||
// * Verify LDAP login succeeds
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
} finally {
|
||||
await pw.deleteLdapUser(ldapUser.username).catch(() => undefined);
|
||||
await adminClient.patchConfig({LdapSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an email/password user can switch their sign-in method to OpenID SSO.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with Keycloak OpenID configured and authentication transfer enabled.
|
||||
*/
|
||||
test('switches an email account to OpenID SSO', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureSiteUrl();
|
||||
await pw.ensureKeycloakOpenId();
|
||||
const keycloakUser = {
|
||||
username: `kc${user.username}`,
|
||||
email: user.email,
|
||||
firstName: user.first_name || 'First',
|
||||
lastName: user.last_name || 'Last',
|
||||
password: 'Password1',
|
||||
};
|
||||
await pw.createKeycloakUser(keycloakUser);
|
||||
|
||||
try {
|
||||
// # Log in as the email user and open Switch to OpenID
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToOpenId();
|
||||
await pw.emailToOAuthPage.toBeVisible();
|
||||
|
||||
// # Confirm the current password and complete Keycloak login
|
||||
await pw.emailToOAuthPage.submit(user.password);
|
||||
await pw.keycloakLoginPage.login(keycloakUser.username, keycloakUser.password);
|
||||
|
||||
// * Verify the account now authenticates through OpenID
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
const switched = await adminClient.getUser(user.id);
|
||||
expect(switched.auth_service).toBe('openid');
|
||||
} finally {
|
||||
await adminClient.patchConfig({OpenIdSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an AD/LDAP user can switch their sign-in method to email and password.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with OpenLDAP configured and authentication transfer enabled.
|
||||
*/
|
||||
test('switches an AD/LDAP account to email and password', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await pw.ensureOpenldap();
|
||||
const ldapUser = pw.generateLdapUser('claimldap');
|
||||
const newPassword = pw.newTestPassword();
|
||||
|
||||
try {
|
||||
await pw.createLdapUser(ldapUser);
|
||||
|
||||
// # Provision the user through LDAP login
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.submitCredentials(ldapUser.username, ldapUser.password);
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
|
||||
const provisionedUser = await adminClient.getUserByUsername(ldapUser.username);
|
||||
await adminClient.addToTeam(team.id, provisionedUser.id);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
// # Open Switch to Email and Password and set a new password
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToEmail();
|
||||
await pw.ldapToEmailPage.toBeVisible();
|
||||
await pw.ldapToEmailPage.submit(ldapUser.password, newPassword);
|
||||
|
||||
// * Verify the account now authenticates with email/password
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
const switched = await adminClient.getUser(provisionedUser.id);
|
||||
expect(switched.auth_service).toBe('');
|
||||
|
||||
// # Log in with the new email password
|
||||
await pw.loginPage.submitCredentials(provisionedUser.email, newPassword);
|
||||
|
||||
// * Verify email login succeeds
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
} finally {
|
||||
await pw.deleteLdapUser(ldapUser.username).catch(() => undefined);
|
||||
await adminClient.patchConfig({LdapSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an OpenID SSO user can switch their sign-in method to email and password.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with Keycloak OpenID configured and authentication transfer enabled.
|
||||
*/
|
||||
test('switches an OpenID account to email and password', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await pw.ensureSiteUrl();
|
||||
await pw.ensureKeycloakOpenId();
|
||||
const keycloakUser = pw.generateKeycloakUser('openidclaim');
|
||||
await pw.createKeycloakUser(keycloakUser);
|
||||
const newPassword = pw.newTestPassword();
|
||||
|
||||
try {
|
||||
// # Provision the user through OpenID SSO
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.openIdLoginButton.click();
|
||||
await pw.keycloakLoginPage.login(keycloakUser.username, keycloakUser.password);
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
|
||||
const provisionedUser = await adminClient.getUserByUsername(keycloakUser.username);
|
||||
await adminClient.addToTeam(team.id, provisionedUser.id);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
// # Open Switch to Email and Password and set a new password
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToEmail();
|
||||
await pw.oauthToEmailPage.toBeVisible();
|
||||
await pw.oauthToEmailPage.submit(newPassword);
|
||||
|
||||
// * Verify the account now authenticates with email/password
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
const switched = await adminClient.getUser(provisionedUser.id);
|
||||
expect(switched.auth_service).toBe('');
|
||||
|
||||
// # Log in with the new email password
|
||||
await pw.loginPage.submitCredentials(provisionedUser.email, newPassword);
|
||||
|
||||
// * Verify email login succeeds
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
} finally {
|
||||
await adminClient.patchConfig({OpenIdSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an email account can switch to OpenID and back to email in one session.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with Keycloak OpenID configured and authentication transfer enabled.
|
||||
*/
|
||||
test('round-trips an email account through OpenID and back to email', {tag: '@authentication'}, async ({pw}) => {
|
||||
test.setTimeout(pw.duration.two_min);
|
||||
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureSiteUrl();
|
||||
await pw.ensureKeycloakOpenId();
|
||||
const keycloakUser = {
|
||||
username: `kc${user.username}`,
|
||||
email: user.email,
|
||||
firstName: user.first_name || 'First',
|
||||
lastName: user.last_name || 'Last',
|
||||
password: 'Password1',
|
||||
};
|
||||
await pw.createKeycloakUser(keycloakUser);
|
||||
const newPassword = pw.newTestPassword();
|
||||
|
||||
try {
|
||||
// # Log in as the email user and switch to OpenID
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
let profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToOpenId();
|
||||
await pw.emailToOAuthPage.toBeVisible();
|
||||
await pw.emailToOAuthPage.submit(user.password);
|
||||
await pw.keycloakLoginPage.login(keycloakUser.username, keycloakUser.password);
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
|
||||
const afterOpenId = await adminClient.getUser(user.id);
|
||||
expect(afterOpenId.auth_service).toBe('openid');
|
||||
|
||||
// # Log in through OpenID and switch back to email
|
||||
await pw.loginPage.openIdLoginButton.click();
|
||||
await pw.keycloakLoginPage.loginIfFormShown(keycloakUser.username, keycloakUser.password);
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToEmail();
|
||||
await pw.oauthToEmailPage.toBeVisible();
|
||||
await pw.oauthToEmailPage.submit(newPassword);
|
||||
await pw.loginPage.expectOnLoginPage();
|
||||
|
||||
// * Verify the account authenticates with the new email password
|
||||
const afterEmail = await adminClient.getUser(user.id);
|
||||
expect(afterEmail.auth_service).toBe('');
|
||||
await pw.loginPage.submitCredentials(user.email, newPassword);
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
} finally {
|
||||
await adminClient.patchConfig({OpenIdSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify Sign-in Method is hidden when authentication transfer is disabled.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with OpenID enabled so a switch option would otherwise be shown.
|
||||
*/
|
||||
test('hides Sign-in Method when authentication transfer is disabled', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureSiteUrl();
|
||||
await pw.ensureKeycloakOpenId();
|
||||
|
||||
try {
|
||||
// # Disable authentication transfer while another sign-in method is available
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {ExperimentalEnableAuthenticationTransfer: false},
|
||||
});
|
||||
|
||||
// # Log in and open Profile -> Security
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
|
||||
// * Verify the Sign-in Method section is not offered
|
||||
await expect(profileModal.securityTab.signInHeading).toBeHidden();
|
||||
await expect(profileModal.securityTab.editSignInMethod).toBeHidden();
|
||||
} finally {
|
||||
await adminClient.patchConfig({
|
||||
OpenIdSettings: {Enable: false},
|
||||
ServiceSettings: {ExperimentalEnableAuthenticationTransfer: true},
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify switching from email to OpenID is rejected when the current password is wrong.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with Keycloak OpenID configured and authentication transfer enabled.
|
||||
*/
|
||||
test('rejects email-to-OpenID when the current password is wrong', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureSiteUrl();
|
||||
await pw.ensureKeycloakOpenId();
|
||||
|
||||
try {
|
||||
// # Log in as the email user and open Switch to OpenID
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToOpenId();
|
||||
await pw.emailToOAuthPage.toBeVisible();
|
||||
|
||||
// # Submit the wrong current password
|
||||
await pw.emailToOAuthPage.submit('WrongPassword1');
|
||||
|
||||
// * Verify the switch is rejected and the auth method is unchanged
|
||||
await expect(pw.emailToOAuthPage.invalidPasswordError).toBeVisible();
|
||||
const unchanged = await adminClient.getUser(user.id);
|
||||
expect(unchanged.auth_service).toBe('');
|
||||
} finally {
|
||||
await adminClient.patchConfig({OpenIdSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify switching from email to AD/LDAP is rejected when the current password is wrong.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with OpenLDAP configured and authentication transfer enabled.
|
||||
*/
|
||||
test('rejects email-to-LDAP when the current password is wrong', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, user, team} = await pw.initSetup();
|
||||
await pw.ensureOpenldap();
|
||||
const ldapUser = pw.generateLdapUser('claimbad');
|
||||
|
||||
try {
|
||||
await pw.createLdapUser(ldapUser);
|
||||
|
||||
// # Log in as the email user and open Switch to AD/LDAP
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
await pw.channelsPage.goto(team.name);
|
||||
await pw.channelsPage.toBeVisible();
|
||||
|
||||
const profileModal = await pw.channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
await profileModal.securityTab.clickSwitchToLdap();
|
||||
await pw.emailToLdapPage.toBeVisible();
|
||||
|
||||
// # Submit the wrong email password with valid LDAP credentials
|
||||
await pw.emailToLdapPage.submit('WrongPassword1', ldapUser.username, ldapUser.password);
|
||||
|
||||
// * Verify the switch is rejected and the auth method is unchanged
|
||||
await expect(pw.emailToLdapPage.invalidPasswordError).toBeVisible();
|
||||
const unchanged = await adminClient.getUser(user.id);
|
||||
expect(unchanged.auth_service).toBe('');
|
||||
} finally {
|
||||
await pw.deleteLdapUser(ldapUser.username).catch(() => undefined);
|
||||
await adminClient.patchConfig({LdapSettings: {Enable: false}});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test, testConfig} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an unenrolled user is sent to MFA setup when MFA is enabled and enforced.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test('MM-T1778 sends an unenrolled user to MFA setup when MFA is enforced', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
// Cleanup needs mmctl --local to disable enforced MFA for an unenrolled admin session.
|
||||
test.skip(!testConfig.mattermostContainerId, 'Skipping test - requires PW_USE_TESTCONTAINERS=true');
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
try {
|
||||
// # Enable and enforce MFA in the System Console
|
||||
await systemConsolePage.gotoMfaSettings();
|
||||
await systemConsolePage.mfaSettings.enableTrue.check();
|
||||
await systemConsolePage.mfaSettings.enforceTrue.check();
|
||||
await systemConsolePage.mfaSettings.save();
|
||||
|
||||
// # Log in as a user who has not enrolled in MFA
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.submitCredentials(user.username, user.password);
|
||||
|
||||
// * Verify the MFA setup page is shown
|
||||
await pw.mfaSetupPage.toBeVisible();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify a system admin can remove MFA from a user who has enrolled.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test('MM-T1781 lets an admin remove MFA from an enrolled user', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
|
||||
try {
|
||||
// # Enable MFA and enroll the user
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableMultifactorAuthentication: true, EnforceMultifactorAuthentication: false},
|
||||
});
|
||||
await pw.enableUserMfa(adminClient, user.id);
|
||||
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the user's action menu and remove MFA
|
||||
await systemConsolePage.gotoUsers();
|
||||
const actions = await systemConsolePage.users.openUserActions(user.username);
|
||||
await actions.clickRemoveMfa();
|
||||
|
||||
// # Log in as that user after MFA is removed
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
|
||||
// * Verify login succeeds without the MFA setup page
|
||||
await pw.mfaSetupPage.toBeHidden();
|
||||
await pw.selectTeamPage.toBeVisible();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify Remove MFA is hidden for a user who has not enrolled in MFA.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test('MM-T1782 hides Remove MFA for a user who has not enrolled', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
|
||||
try {
|
||||
// # Enable MFA without enrolling the user
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableMultifactorAuthentication: true, EnforceMultifactorAuthentication: false},
|
||||
});
|
||||
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the user's action menu
|
||||
await systemConsolePage.gotoUsers();
|
||||
const actions = await systemConsolePage.users.openUserActions(user.username);
|
||||
|
||||
// * Verify Remove MFA is not offered
|
||||
await actions.expectRemoveMfaHidden();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify Multi-factor Authentication is hidden in Profile when MFA is disabled.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test(
|
||||
'MM-T1777 hides MFA in Profile when multifactor authentication is disabled',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {user, adminClient, team} = await pw.initSetup();
|
||||
|
||||
try {
|
||||
// # Disable MFA and open Profile -> Security
|
||||
await pw.disableMfa(adminClient);
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const profileModal = await channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
|
||||
// * Verify the MFA section is not shown
|
||||
await expect(profileModal.securityTab.mfaHeading).toBeHidden();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify Multi-factor Authentication appears in Profile when MFA is enabled.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test(
|
||||
'MM-T1779 shows MFA in Profile when multifactor authentication is enabled',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {user, adminClient, team} = await pw.initSetup();
|
||||
|
||||
try {
|
||||
// # Enable MFA and open Profile -> Security
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableMultifactorAuthentication: true, EnforceMultifactorAuthentication: false},
|
||||
});
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
const profileModal = await channelsPage.openProfileModal();
|
||||
await profileModal.openSecurityTab();
|
||||
|
||||
// * Verify the MFA section is shown
|
||||
await expect(profileModal.securityTab.mfaHeading).toBeVisible();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a user can log in without an MFA prompt when MFA is enabled but not enforced.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA available.
|
||||
*/
|
||||
test('MM-T1780 logs in without an MFA prompt when MFA is not enforced', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
|
||||
try {
|
||||
// # Enable MFA without enforcing it
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableMultifactorAuthentication: true, EnforceMultifactorAuthentication: false},
|
||||
});
|
||||
|
||||
// # Log in as a user who has not enrolled in MFA
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.login(user);
|
||||
|
||||
// * Verify login succeeds without the MFA setup page
|
||||
await pw.mfaSetupPage.toBeHidden();
|
||||
await pw.selectTeamPage.toBeVisible();
|
||||
} finally {
|
||||
await pw.disableMfa(adminClient);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify Minimum password length rejects values outside 5-72 (or 14-72 on FIPS).
|
||||
*/
|
||||
test('MM-T1771 rejects a minimum password length outside the allowed range', {tag: '@authentication'}, async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const originalMinimumLength = (await adminClient.getConfig()).PasswordSettings.MinimumLength;
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
try {
|
||||
await systemConsolePage.gotoPasswordSettings();
|
||||
|
||||
// # Save a length above the maximum
|
||||
await systemConsolePage.passwordSettings.minimumLength.fill('88');
|
||||
await systemConsolePage.passwordSettings.save();
|
||||
|
||||
// * Verify the range error is shown
|
||||
await expect(systemConsolePage.passwordSettings.lengthError).toBeVisible();
|
||||
|
||||
// # Save a length below the minimum
|
||||
await systemConsolePage.passwordSettings.minimumLength.fill('3');
|
||||
await systemConsolePage.passwordSettings.save();
|
||||
|
||||
// * Verify the range error is shown
|
||||
await expect(systemConsolePage.passwordSettings.lengthError).toBeVisible();
|
||||
} finally {
|
||||
await adminClient.patchConfig({PasswordSettings: {MinimumLength: originalMinimumLength}});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify changing minimum password length updates signup help text and validation.
|
||||
*/
|
||||
test('MM-T1772 applies a new minimum password length on signup', {tag: '@authentication'}, async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const originalMinimumLength = (await adminClient.getConfig()).PasswordSettings.MinimumLength;
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
try {
|
||||
await systemConsolePage.gotoPasswordSettings();
|
||||
|
||||
// # Set the minimum length to 15
|
||||
await systemConsolePage.passwordSettings.minimumLength.fill('15');
|
||||
await systemConsolePage.passwordSettings.save();
|
||||
await expect(systemConsolePage.passwordSettings.sampleHelpText).toHaveText(
|
||||
'Your password must be 15-72 characters long.',
|
||||
);
|
||||
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.signupPage.goto();
|
||||
await pw.signupPage.toBeVisible();
|
||||
await pw.signupPage.emailInput.fill(`test-${pw.random.id()}@example.com`);
|
||||
await pw.signupPage.usernameInput.fill(`user${pw.random.id()}`);
|
||||
await pw.signupPage.termsAndPrivacyCheckBox.check();
|
||||
|
||||
// # Submit a password that is too short
|
||||
await pw.signupPage.passwordInput.fill('less');
|
||||
await pw.signupPage.createAccountButton.click();
|
||||
await expect(pw.signupPage.passwordLengthError).toBeVisible();
|
||||
|
||||
// # Submit a password that meets the new minimum
|
||||
await pw.signupPage.passwordInput.fill('GreaterThan15Chr!');
|
||||
await pw.signupPage.termsAndPrivacyCheckBox.check();
|
||||
await pw.signupPage.createAccountButton.click();
|
||||
|
||||
// * Verify signup succeeds
|
||||
await pw.selectTeamPage.toBeVisible();
|
||||
} finally {
|
||||
await adminClient.patchConfig({PasswordSettings: {MinimumLength: originalMinimumLength}});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @objective Verify clearing Minimum password length resets it to the server default.
|
||||
*/
|
||||
test(
|
||||
'MM-T1773 resets Minimum password length to the default after clearing it',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const originalMinimumLength = (await adminClient.getConfig()).PasswordSettings.MinimumLength;
|
||||
const customLength = originalMinimumLength === 20 ? 21 : 20;
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
try {
|
||||
await systemConsolePage.gotoPasswordSettings();
|
||||
|
||||
// # Save a custom minimum length
|
||||
await systemConsolePage.passwordSettings.minimumLength.fill(String(customLength));
|
||||
await systemConsolePage.passwordSettings.save();
|
||||
await systemConsolePage.passwordSettings.reload();
|
||||
await expect(systemConsolePage.passwordSettings.minimumLength).toHaveValue(String(customLength));
|
||||
|
||||
// # Clear the field and save
|
||||
await systemConsolePage.passwordSettings.minimumLength.clear();
|
||||
await systemConsolePage.passwordSettings.save();
|
||||
await systemConsolePage.passwordSettings.reload();
|
||||
|
||||
// * Verify the saved value resets to the compiled default, not the on-prem override
|
||||
const resetLength = (await adminClient.getConfig()).PasswordSettings.MinimumLength;
|
||||
expect(resetLength).toBe(5);
|
||||
await expect(systemConsolePage.passwordSettings.minimumLength).toHaveValue(String(resetLength));
|
||||
} finally {
|
||||
await adminClient.patchConfig({PasswordSettings: {MinimumLength: originalMinimumLength}});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify all password-complexity requirements are enforced on signup.
|
||||
*/
|
||||
test(
|
||||
'MM-T1774 rejects signup passwords that miss a required character class',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
const {adminClient} = await pw.initSetup();
|
||||
|
||||
try {
|
||||
await adminClient.patchConfig({
|
||||
PasswordSettings: {Lowercase: true, Number: true, Uppercase: true, Symbol: true},
|
||||
});
|
||||
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.signupPage.goto();
|
||||
await pw.signupPage.toBeVisible();
|
||||
await pw.signupPage.emailInput.fill(`test-${pw.random.id()}@example.com`);
|
||||
await pw.signupPage.usernameInput.fill(`user${pw.random.id()}`);
|
||||
await pw.signupPage.termsAndPrivacyCheckBox.check();
|
||||
|
||||
for (const password of ['NOLOWERCASE12345!', 'nouppercase12345!', 'NoNumberHere!!!', 'NoSymbol1234567']) {
|
||||
// # Submit a password missing a required character class
|
||||
await pw.signupPage.passwordInput.fill(password);
|
||||
await pw.signupPage.createAccountButton.click();
|
||||
|
||||
// * Verify the complexity error is shown
|
||||
await expect(pw.signupPage.passwordComplexityError).toBeVisible();
|
||||
}
|
||||
} finally {
|
||||
await adminClient.patchConfig({
|
||||
PasswordSettings: {Lowercase: false, Number: false, Uppercase: false, Symbol: false},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify a basic-auth user's System Console profile shows Authentication Method Email.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with the User Detail page available.
|
||||
*/
|
||||
test(
|
||||
'MM-T953_1 shows Email as the authentication method for a basic-auth user',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the user's System Console profile
|
||||
await systemConsolePage.gotoUser(user.id);
|
||||
|
||||
// * Verify the authentication method is Email
|
||||
await systemConsolePage.users.userDetail.userCard.expectAuthenticationMethod('Email');
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify an MFA-enrolled basic-auth user's System Console profile shows Email, MFA.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with MFA enabled.
|
||||
*/
|
||||
test('MM-T953_2 shows Email, MFA when multifactor authentication is active', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const user = await pw.createNewUserProfile(adminClient);
|
||||
|
||||
try {
|
||||
// # Enable MFA and enroll the user
|
||||
await adminClient.patchConfig({ServiceSettings: {EnableMultifactorAuthentication: true}});
|
||||
await pw.enableUserMfa(adminClient, user.id);
|
||||
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the user's System Console profile
|
||||
await systemConsolePage.gotoUser(user.id);
|
||||
|
||||
// * Verify the authentication method includes MFA
|
||||
await systemConsolePage.users.userDetail.userCard.expectAuthenticationMethod('Email, MFA');
|
||||
} finally {
|
||||
await adminClient.patchConfig({ServiceSettings: {EnableMultifactorAuthentication: false}});
|
||||
}
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify an LDAP-provisioned user's System Console profile shows Authentication Method LDAP.
|
||||
*
|
||||
* @precondition
|
||||
* An LDAP directory reachable at the configured LdapSettings.LdapServer/LdapPort.
|
||||
*/
|
||||
test(
|
||||
'MM-T953_3 shows LDAP as the authentication method for a directory user',
|
||||
{tag: '@authentication'},
|
||||
async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
await pw.ensureOpenldap();
|
||||
const ldapUser = pw.generateLdapUser();
|
||||
await pw.createLdapUser(ldapUser);
|
||||
|
||||
// # Provision the directory user through the login form
|
||||
await pw.hasSeenLandingPage();
|
||||
await pw.loginPage.goto();
|
||||
await pw.loginPage.toBeVisible();
|
||||
await pw.loginPage.submitCredentials(ldapUser.username, ldapUser.password);
|
||||
await pw.loginPage.expectNotOnLoginPage();
|
||||
|
||||
const provisionedUser = await adminClient.getUserByUsername(ldapUser.username);
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the LDAP user's System Console profile
|
||||
await systemConsolePage.gotoUser(provisionedUser.id);
|
||||
|
||||
// * Verify the authentication method is LDAP
|
||||
await systemConsolePage.users.userDetail.userCard.expectAuthenticationMethod('LDAP');
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify a SAML user's System Console profile shows Authentication Method SAML.
|
||||
*
|
||||
* @precondition
|
||||
* A licensed server with the User Detail page available.
|
||||
*/
|
||||
test('MM-T953_4 shows SAML as the authentication method for a SAML user', {tag: '@authentication'}, async ({pw}) => {
|
||||
await pw.ensureLicense();
|
||||
await pw.skipIfNoLicense();
|
||||
|
||||
const {adminUser, adminClient, user} = await pw.initSetup();
|
||||
await adminClient.updateUserAuth(user.id, {auth_service: 'saml', auth_data: user.email});
|
||||
|
||||
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Open the SAML user's System Console profile
|
||||
await systemConsolePage.gotoUser(user.id);
|
||||
|
||||
// * Verify the authentication method is SAML
|
||||
await systemConsolePage.users.userDetail.userCard.expectAuthenticationMethod('SAML');
|
||||
});
|
||||
Reference in new issue
Block a user