snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* Verify that profile popover shows correct user information based on privacy settings
|
||||
* and continues to show correct information after at-mention autocomplete is used.
|
||||
*
|
||||
* Precondition:
|
||||
* 1. A test server configured with privacy settings to hide email and full name
|
||||
* 2. Two user accounts, with one user able to see their own information
|
||||
*/
|
||||
test('Profile popover should show correct fields after at-mention autocomplete @user_profile', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
// Initialize with user's privacy settings set to hide email and full name
|
||||
const {user, adminClient, team} = await pw.initSetup();
|
||||
await adminClient.patchConfig({
|
||||
PrivacySettings: {
|
||||
ShowEmailAddress: false,
|
||||
ShowFullName: false,
|
||||
},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.PrivacySettings?.ShowEmailAddress === false && cfg.PrivacySettings?.ShowFullName === false;
|
||||
});
|
||||
|
||||
// Create and add another user using admin client
|
||||
const testUser2 = await adminClient.createUser(await pw.random.user('other'), '', '');
|
||||
await adminClient.addToTeam(team.id, testUser2.id);
|
||||
|
||||
// 1. Login as the first user
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// 2. Post a message with mentions of both users
|
||||
await channelsPage.postMessage(`@${user.username} @${testUser2.username}`);
|
||||
|
||||
// 3. Open profile popover for the current user on first
|
||||
const lastPost = await channelsPage.getLastPost();
|
||||
await expect(lastPost.container).toContainText(`@${user.username}`);
|
||||
await expect(lastPost.container).toContainText(`@${testUser2.username}`);
|
||||
const firstMention = await lastPost.container.getByText(`@${user.username}`, {exact: true});
|
||||
await firstMention.click();
|
||||
const currentUserProfilePopover = channelsPage.userProfilePopover;
|
||||
|
||||
// * Verify all fields are visible for current user in the profile popover
|
||||
await expect(currentUserProfilePopover.container.getByText(`@${user.username}`)).toBeVisible();
|
||||
await expect(currentUserProfilePopover.container.getByText(`${user.first_name} ${user.last_name}`)).toBeVisible();
|
||||
await expect(currentUserProfilePopover.container.getByText(user.email)).toBeVisible();
|
||||
|
||||
// 4. Close the current user's profile popover
|
||||
await currentUserProfilePopover.close();
|
||||
|
||||
// 5. Open profile popover for the other user on second mention.
|
||||
// Re-apply privacy settings in case a concurrent initSetup() reset them,
|
||||
// then wait until the server confirms the value before proceeding.
|
||||
await adminClient.patchConfig({
|
||||
PrivacySettings: {
|
||||
ShowEmailAddress: false,
|
||||
ShowFullName: false,
|
||||
},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.PrivacySettings?.ShowEmailAddress === false && cfg.PrivacySettings?.ShowFullName === false;
|
||||
});
|
||||
|
||||
// Reload the page so the browser fetches the new config synchronously.
|
||||
// waitUntil above only confirms the server-side state; the browser updates its
|
||||
// Redux store via WebSocket (CONFIG_CHANGED event) which can lag significantly.
|
||||
// A full page reload forces a fresh /api/v4/config/client fetch, so the privacy
|
||||
// settings are guaranteed to be in effect before we render any popover.
|
||||
await channelsPage.page.reload();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// Re-locate the post after reload (DOM was replaced)
|
||||
const lastPostAfterReload = await channelsPage.getLastPost();
|
||||
await expect(lastPostAfterReload.container).toContainText(`@${testUser2.username}`);
|
||||
const secondMention = lastPostAfterReload.container.getByText(`@${testUser2.username}`, {exact: true});
|
||||
await secondMention.click();
|
||||
const otherUserProfilePopover = channelsPage.userProfilePopover;
|
||||
|
||||
// * Verify only username is visible for other user in the profile popover
|
||||
await expect(otherUserProfilePopover.container.getByText(`@${testUser2.username}`)).toBeVisible();
|
||||
await expect(otherUserProfilePopover.container.getByText(testUser2.email)).not.toBeVisible();
|
||||
|
||||
// 6. Close the other user's profile popover
|
||||
await otherUserProfilePopover.close();
|
||||
|
||||
// # Type an at-mention so autocomplete searches on each keystroke
|
||||
await channelsPage.centerView.postCreate.typeMessage(`@${user.username}`);
|
||||
|
||||
// * Verify autocomplete suggestion appears
|
||||
const suggestionList = channelsPage.centerView.postCreate.suggestionList;
|
||||
await expect(suggestionList).toBeVisible();
|
||||
await expect(suggestionList.getByText(`@${user.username}`)).toBeVisible();
|
||||
|
||||
// 8. Clear the message box and wait for the autocomplete overlay to fully
|
||||
// disappear before interacting with the post — the overlay can otherwise
|
||||
// block hover/click events on the message area and cause a flaky failure.
|
||||
await channelsPage.centerView.postCreate.writeMessage('');
|
||||
await expect(suggestionList).toBeHidden({timeout: 5000});
|
||||
|
||||
// 9. Open profile popover by clicking the @mention text directly (same
|
||||
// approach as steps 3-4) — more reliable than openProfilePopover() which
|
||||
// uses a hover-then-click sequence that the overlay can intercept.
|
||||
const currentUserMention = lastPostAfterReload.container.getByText(`@${user.username}`, {exact: true});
|
||||
await currentUserMention.click();
|
||||
const profilePopoverAgain = channelsPage.userProfilePopover;
|
||||
|
||||
// * Verify all fields are still visible
|
||||
await expect(profilePopoverAgain.container.getByText(`@${user.username}`)).toBeVisible();
|
||||
await expect(profilePopoverAgain.container.getByText(`${user.first_name} ${user.last_name}`)).toBeVisible();
|
||||
await expect(profilePopoverAgain.container.getByText(user.email)).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Locator, Page} from '@playwright/test';
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* E2E coverage for the Personal Access Token (PAT) expiry UI added in MM-68421.
|
||||
*
|
||||
* Enforcement is implied by ServiceSettings.MaximumPersonalAccessTokenLifetimeDays:
|
||||
* 0 means tokens may never expire; a value > 0 requires every token to expire
|
||||
* within that many days (there is no separate "enforce expiry" flag on the server).
|
||||
*
|
||||
* The expired-status badge is intentionally not covered here: the server rejects
|
||||
* creating a token whose expiry is already in the past, so an expired token cannot
|
||||
* be seeded via the API. That branch is exercised by the component unit tests.
|
||||
*/
|
||||
|
||||
const TOKEN_ROLES = 'system_user system_user_access_token';
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// YYYY-MM-DD, n days from today in local time (matches the <input type="date"> format).
|
||||
function isoPlusDays(n: number): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + n);
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Enables tokens and sets the maximum lifetime, then waits for the server to report both. The
|
||||
// maximum is server-wide, so every test states the value it needs rather than inheriting one.
|
||||
async function setTokenPolicy(adminClient: Client4, maximumLifetimeDays: number) {
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableUserAccessTokens: true, MaximumPersonalAccessTokenLifetimeDays: maximumLifetimeDays},
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const {ServiceSettings} = await adminClient.getConfig();
|
||||
return {
|
||||
enabled: ServiceSettings?.EnableUserAccessTokens,
|
||||
maximumLifetimeDays: ServiceSettings?.MaximumPersonalAccessTokenLifetimeDays,
|
||||
};
|
||||
})
|
||||
.toEqual({enabled: true, maximumLifetimeDays});
|
||||
}
|
||||
|
||||
// Open Account Settings > Security and expand the Personal Access Tokens section.
|
||||
async function openTokensSection(page: Page): Promise<Locator> {
|
||||
await page.locator('#userAccountMenuButton').click();
|
||||
await page.getByRole('menuitem', {name: 'Profile'}).click();
|
||||
|
||||
const profileModal = page.getByRole('dialog', {name: 'Profile'});
|
||||
await expect(profileModal).toBeVisible();
|
||||
|
||||
await profileModal.getByRole('tab', {name: 'Security'}).click();
|
||||
await profileModal.locator('#tokensEdit').click();
|
||||
|
||||
return profileModal;
|
||||
}
|
||||
|
||||
test.describe('Personal Access Tokens expiry @personal_access_tokens', () => {
|
||||
test('shows the expiry picker with all presets and reveals the custom date input', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Personal Access Tokens section and start creating a token
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
await modal.getByRole('button', {name: 'Create Token'}).click();
|
||||
|
||||
// * The expiry select offers "No expiry", every preset, and a custom option
|
||||
const expirySelect = modal.locator('#newTokenExpiry');
|
||||
await expect(expirySelect).toBeVisible();
|
||||
await expect(expirySelect.locator('option', {hasText: 'No expiry'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: '7 days'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: '30 days'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: '90 days'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: '1 year'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: /Custom date/})).toHaveCount(1);
|
||||
|
||||
// * The custom date input is hidden until the custom option is chosen
|
||||
await expect(modal.locator('#newTokenExpiryCustom')).toBeHidden();
|
||||
await expirySelect.selectOption('custom');
|
||||
await expect(modal.locator('#newTokenExpiryCustom')).toBeVisible();
|
||||
});
|
||||
|
||||
test('blocks submitting a custom expiry with no date chosen', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
await modal.getByRole('button', {name: 'Create Token'}).click();
|
||||
|
||||
// # Provide a description, pick the custom preset, then clear the date
|
||||
await modal.locator('#newTokenDescription').fill('My token');
|
||||
await modal.locator('#newTokenExpiry').selectOption('custom');
|
||||
await modal.locator('#newTokenExpiryCustom').fill('');
|
||||
|
||||
// * The inline validation error surfaces and Save is disabled, so no token can be created
|
||||
await expect(modal.getByText('An expiry date is required.')).toBeVisible();
|
||||
await expect(modal.getByRole('button', {name: 'Save'})).toBeDisabled();
|
||||
await expect(modal.getByText('Access Token:')).toBeHidden();
|
||||
});
|
||||
|
||||
test('enforces expiry when a maximum lifetime is configured', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 30);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
await modal.getByRole('button', {name: 'Create Token'}).click();
|
||||
|
||||
// * "No expiry" and presets longer than the maximum are hidden; the enforced hint shows
|
||||
const expirySelect = modal.locator('#newTokenExpiry');
|
||||
await expect(expirySelect.locator('option', {hasText: 'No expiry'})).toHaveCount(0);
|
||||
await expect(expirySelect.locator('option', {hasText: '90 days'})).toHaveCount(0);
|
||||
await expect(expirySelect.locator('option', {hasText: '1 year'})).toHaveCount(0);
|
||||
await expect(expirySelect.locator('option', {hasText: '7 days'})).toHaveCount(1);
|
||||
await expect(expirySelect.locator('option', {hasText: '30 days'})).toHaveCount(1);
|
||||
await expect(
|
||||
modal.getByText('Your administrator requires all personal access tokens to have an expiry date.'),
|
||||
).toBeVisible();
|
||||
|
||||
// # Choose a custom date beyond the configured maximum
|
||||
await modal.locator('#newTokenDescription').fill('My token');
|
||||
await expirySelect.selectOption('custom');
|
||||
await modal.locator('#newTokenExpiryCustom').fill(isoPlusDays(60));
|
||||
|
||||
// * The over-the-limit error surfaces inline and Save is disabled
|
||||
await expect(modal.getByText('Expiry can be at most 30 days from now.')).toBeVisible();
|
||||
await expect(modal.getByRole('button', {name: 'Save'})).toBeDisabled();
|
||||
});
|
||||
|
||||
test('creates a token with the default preset under a maximum lifetime', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 30);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
await modal.getByRole('button', {name: 'Create Token'}).click();
|
||||
|
||||
// # Accept the default preset (which equals the cap) and save
|
||||
await modal.locator('#newTokenDescription').fill('My token');
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// * The token is created (the server accepts the clamped expiry) and revealed
|
||||
await expect(modal.getByText('Access Token:')).toBeVisible();
|
||||
await expect(modal.getByText('Expiry can be at most 30 days from now.')).toBeHidden();
|
||||
});
|
||||
|
||||
test('shows status and expiry for existing tokens', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
// # Seed three tokens for the user: never-expiring, expiring soon, and disabled
|
||||
await adminClient.createUserAccessToken(user.id, 'never expires token');
|
||||
await adminClient.createUserAccessToken(user.id, 'expiring soon token', Date.now() + 3 * DAY_MS);
|
||||
const disabledToken = await adminClient.createUserAccessToken(user.id, 'disabled token');
|
||||
await adminClient.disableUserAccessToken(disabledToken.id);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
|
||||
// * The never-expiring token is Active and shows "Never"
|
||||
const neverRow = modal.locator('.setting-box__item', {hasText: 'never expires token'});
|
||||
await expect(neverRow.getByText('Active')).toBeVisible();
|
||||
await expect(neverRow.getByText(/Never/)).toBeVisible();
|
||||
|
||||
// * The soon-expiring token is Active and shows an "expires in N days" warning
|
||||
const soonRow = modal.locator('.setting-box__item', {hasText: 'expiring soon token'});
|
||||
await expect(soonRow.getByText('Active')).toBeVisible();
|
||||
await expect(soonRow.getByText(/Expires in \d+ days?/)).toBeVisible();
|
||||
|
||||
// * The disabled token shows the Disabled badge
|
||||
const disabledRow = modal.locator('.setting-box__item', {hasText: 'disabled token'});
|
||||
await expect(disabledRow.getByText('Disabled')).toBeVisible();
|
||||
});
|
||||
|
||||
test('regenerates a token and reveals the new secret', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
// # Seed a token for the user
|
||||
const originalToken = await adminClient.createUserAccessToken(user.id, 'rotate me token');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
const tokenRow = modal.locator('.setting-box__item', {hasText: 'rotate me token'});
|
||||
await expect(tokenRow.getByText(originalToken.id)).toBeVisible();
|
||||
|
||||
// # Click Regenerate and confirm the warning naming the token
|
||||
await tokenRow.getByRole('link', {name: 'Regenerate'}).click();
|
||||
const confirmModal = channelsPage.page.locator('#confirmModal');
|
||||
await expect(confirmModal.getByText('Regenerate Token?')).toBeVisible();
|
||||
await expect(confirmModal.getByText(/rotate me token/)).toBeVisible();
|
||||
await confirmModal.getByRole('button', {name: 'Yes, Regenerate'}).click();
|
||||
|
||||
// * The new secret is revealed, the token keeps its id, and the old row is hidden
|
||||
await expect(modal.getByText('Access Token:')).toBeVisible();
|
||||
await expect(modal.getByText(originalToken.id)).toBeVisible();
|
||||
await expect(tokenRow).toBeHidden();
|
||||
|
||||
// * The old secret no longer authenticates, the token is still enabled
|
||||
const rotated = await adminClient.getUserAccessToken(originalToken.id);
|
||||
expect(rotated.is_active).toBe(true);
|
||||
expect(rotated.id).toBe(originalToken.id);
|
||||
});
|
||||
|
||||
test('lets the user pick a new expiry when regenerating', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
// # Seed a never-expiring token for the user
|
||||
const originalToken = await adminClient.createUserAccessToken(user.id, 'expiring rotate token');
|
||||
expect(originalToken.expires_at).toBe(0);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
const tokenRow = modal.locator('.setting-box__item', {hasText: 'expiring rotate token'});
|
||||
|
||||
// # Regenerate, choosing a 7-day expiry instead of the "no expiry" default
|
||||
await tokenRow.getByRole('link', {name: 'Regenerate'}).click();
|
||||
const confirmModal = channelsPage.page.locator('#confirmModal');
|
||||
await confirmModal.locator('#regenerateTokenExpiry').selectOption('7d');
|
||||
await confirmModal.getByRole('button', {name: 'Yes, Regenerate'}).click();
|
||||
|
||||
// * The rotated token now has a future expiry set
|
||||
await expect(modal.getByText('Access Token:')).toBeVisible();
|
||||
const rotated = await adminClient.getUserAccessToken(originalToken.id);
|
||||
expect(rotated.expires_at).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
test('does not offer to regenerate a disabled token', async ({pw}) => {
|
||||
test.setTimeout(120000);
|
||||
const {user, adminClient} = await pw.initSetup();
|
||||
await setTokenPolicy(adminClient, 0);
|
||||
await adminClient.updateUserRoles(user.id, TOKEN_ROLES);
|
||||
|
||||
const disabledToken = await adminClient.createUserAccessToken(user.id, 'disabled token');
|
||||
await adminClient.disableUserAccessToken(disabledToken.id);
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const modal = await openTokensSection(channelsPage.page);
|
||||
const disabledRow = modal.locator('.setting-box__item', {hasText: 'disabled token'});
|
||||
|
||||
// * No Regenerate link for a disabled token
|
||||
await expect(disabledRow.getByRole('link', {name: 'Regenerate'})).toBeHidden();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user