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 Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
210 lines
9.7 KiB
TypeScript
210 lines
9.7 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {expect, test, testConfig} from '@mattermost/playwright-lib';
|
|
|
|
async function postToWebhook(webhookId: string, payload: Record<string, unknown>) {
|
|
const hookUrl = `${testConfig.baseURL}/hooks/${webhookId}`;
|
|
const resp = await fetch(hookUrl, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
throw new Error(`Webhook POST failed: ${resp.status} ${await resp.text()}`);
|
|
}
|
|
}
|
|
|
|
function notificationText(n: {title?: string; body?: string}) {
|
|
return `${n.title ?? ''}\n${n.body ?? ''}`;
|
|
}
|
|
|
|
function channelHasMessage(posts: {posts: Record<string, {message?: string}>}, message: string) {
|
|
return Object.values(posts.posts).some((p) => p.message?.includes(message));
|
|
}
|
|
|
|
test.describe('Silent notification webhook delivery', () => {
|
|
/**
|
|
* @objective Verify integration posts with silent=true are delivered in-channel without unread or desktop notify side effects.
|
|
*
|
|
* @precondition
|
|
* - User is a member of a dedicated channel with an incoming webhook
|
|
* - User is viewing a different channel so webhook posts are not auto-marked read
|
|
* - Desktop notify level is mention-based (default); messages include @username
|
|
*/
|
|
test(
|
|
'incoming webhook silent post is visible without unread or desktop notification',
|
|
{tag: ['@notifications']},
|
|
async ({pw, headless, browserName}) => {
|
|
test.skip(
|
|
headless && browserName !== 'firefox' && browserName !== 'webkit',
|
|
'In headless mode, Notification stubbing is only reliable in Firefox (and WebKit when configured). Use npm run playwright-ui (headed), --project=firefox, or PW_HEADLESS=false.',
|
|
);
|
|
|
|
const {team, user, adminClient} = await pw.initSetup();
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({
|
|
teamId: team.id,
|
|
name: 'silent-hook',
|
|
displayName: 'Silent Hook Test',
|
|
unique: true,
|
|
}),
|
|
);
|
|
await adminClient.addToChannel(user.id, channel.id);
|
|
|
|
const webhook = await adminClient.createIncomingWebhook({
|
|
channel_id: channel.id,
|
|
display_name: 'Silent E2E Hook',
|
|
});
|
|
|
|
const {page, channelsPage} = await pw.testBrowser.login(user);
|
|
|
|
// # Mark the webhook channel as read, then view another channel
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await channelsPage.toBeVisible();
|
|
await channelsPage.goto(team.name, 'off-topic');
|
|
await channelsPage.toBeVisible();
|
|
|
|
await pw.stubNotification(page, 'granted');
|
|
|
|
const channelLink = channelsPage.sidebarLeft.container.locator(`#sidebarItem_${channel.name}`);
|
|
|
|
// # Post a normal webhook message with @mention to verify unread + notification baseline
|
|
const normalMessage = `normal webhook @${user.username} ${Date.now()}`;
|
|
await postToWebhook(webhook.id, {text: normalMessage});
|
|
await expect(channelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
|
await expect
|
|
.poll(async () => (await page.evaluate(() => window.getNotifications())).length)
|
|
.toBeGreaterThan(0);
|
|
|
|
await channelsPage.sidebarLeft.goToItem(channel.name);
|
|
const normalPost = await channelsPage.getLastPost();
|
|
await normalPost.toContainText(normalMessage);
|
|
|
|
await channelsPage.goto(team.name, 'off-topic');
|
|
await channelsPage.toBeVisible();
|
|
await expect(channelLink).not.toHaveClass(/unread-title/);
|
|
|
|
const syncChannel = await adminClient.createChannel(
|
|
pw.random.channel({
|
|
teamId: team.id,
|
|
name: 'sync-pulse',
|
|
displayName: 'Sync Pulse',
|
|
unique: true,
|
|
}),
|
|
);
|
|
await adminClient.addToChannel(user.id, syncChannel.id);
|
|
const syncChannelLink = channelsPage.sidebarLeft.container.locator(`#sidebarItem_${syncChannel.name}`);
|
|
|
|
await pw.clearCapturedNotifications(page);
|
|
|
|
// # Post silent webhook, then a normal post on another channel to flush the WS pipeline (FIFO)
|
|
const silentMessage = `silent webhook @${user.username} ${Date.now()}`;
|
|
await postToWebhook(webhook.id, {text: silentMessage, silent: true});
|
|
|
|
await expect
|
|
.poll(async () => channelHasMessage(await adminClient.getPosts(channel.id, 0, 30), silentMessage))
|
|
.toBe(true);
|
|
|
|
const pulseMessage = `pulse ${Date.now()}`;
|
|
await adminClient.createPost({channel_id: syncChannel.id, message: pulseMessage});
|
|
|
|
await expect(syncChannelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
|
await expect(channelLink).not.toHaveClass(/unread-title/);
|
|
|
|
const notificationsAfterSilent = await page.evaluate(() => window.getNotifications());
|
|
expect(notificationsAfterSilent.some((n) => notificationText(n).includes(silentMessage))).toBe(false);
|
|
|
|
// * Silent post is still visible; no New Messages separator (final assertions — goToItem marks viewed)
|
|
await channelsPage.sidebarLeft.goToItem(channel.name);
|
|
const silentPost = await channelsPage.getLastPost();
|
|
await silentPost.toContainText(silentMessage);
|
|
await expect(page.locator('[data-testid="NotificationSeparator"]')).not.toBeVisible();
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @objective Verify a user @mentioned in a silent webhook post does not receive a desktop notification.
|
|
*/
|
|
test(
|
|
'silent webhook @mention does not desktop-notify the mentioned user',
|
|
{tag: ['@notifications']},
|
|
async ({pw, headless, browserName}) => {
|
|
test.skip(
|
|
headless && browserName !== 'firefox' && browserName !== 'webkit',
|
|
'Desktop notification stubbing is only reliable in headless Firefox and WebKit.',
|
|
);
|
|
|
|
const {team, user, adminClient} = await pw.initSetup();
|
|
|
|
const mentionedUser = await pw.createNewUserProfile(adminClient);
|
|
await adminClient.addToTeam(team.id, mentionedUser.id);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({
|
|
teamId: team.id,
|
|
name: 'silent-mention',
|
|
displayName: 'Silent Mention Test',
|
|
unique: true,
|
|
}),
|
|
);
|
|
await adminClient.addToChannel(user.id, channel.id);
|
|
await adminClient.addToChannel(mentionedUser.id, channel.id);
|
|
|
|
const webhook = await adminClient.createIncomingWebhook({
|
|
channel_id: channel.id,
|
|
display_name: 'Silent Mention Hook',
|
|
});
|
|
|
|
const {channelsPage: posterChannelsPage} = await pw.testBrowser.login(user);
|
|
await posterChannelsPage.goto(team.name, channel.name);
|
|
await posterChannelsPage.toBeVisible();
|
|
await posterChannelsPage.goto(team.name, 'off-topic');
|
|
await posterChannelsPage.toBeVisible();
|
|
|
|
const {page: mentioneePage, channelsPage: mentioneeChannelsPage} =
|
|
await pw.testBrowser.login(mentionedUser);
|
|
await mentioneeChannelsPage.goto(team.name, 'off-topic');
|
|
await mentioneeChannelsPage.toBeVisible();
|
|
await pw.stubNotification(mentioneePage, 'granted');
|
|
|
|
const channelLink = mentioneeChannelsPage.sidebarLeft.container.locator(`#sidebarItem_${channel.name}`);
|
|
|
|
// # Baseline: normal @mention webhook should notify the mentioned user and mark channel unread
|
|
const normalMessage = `normal mention @${mentionedUser.username} ${Date.now()}`;
|
|
await postToWebhook(webhook.id, {text: normalMessage});
|
|
await expect(channelLink).toHaveClass(/unread-title/, {timeout: 15000});
|
|
await expect
|
|
.poll(async () => (await mentioneePage.evaluate(() => window.getNotifications())).length)
|
|
.toBeGreaterThan(0);
|
|
|
|
// # Mark the channel read by opening it, then return to off-topic for a clean baseline
|
|
await mentioneeChannelsPage.sidebarLeft.goToItem(channel.name);
|
|
await mentioneeChannelsPage.toBeVisible();
|
|
await mentioneeChannelsPage.goto(team.name, 'off-topic');
|
|
await mentioneeChannelsPage.toBeVisible();
|
|
await expect(channelLink).not.toHaveClass(/unread-title/);
|
|
|
|
await pw.clearCapturedNotifications(mentioneePage);
|
|
|
|
// # Silent @mention should not notify the mentioned user nor mark the channel unread
|
|
const silentMessage = `silent mention @${mentionedUser.username} ${Date.now()}`;
|
|
await postToWebhook(webhook.id, {text: silentMessage, silent: true});
|
|
|
|
await expect
|
|
.poll(async () => channelHasMessage(await adminClient.getPosts(channel.id, 0, 30), silentMessage))
|
|
.toBe(true);
|
|
|
|
await expect
|
|
.poll(async () => (await mentioneePage.evaluate(() => window.getNotifications())).length)
|
|
.toBe(0);
|
|
await expect(channelLink).not.toHaveClass(/unread-title/);
|
|
|
|
const notificationsAfterSilent = await mentioneePage.evaluate(() => window.getNotifications());
|
|
expect(notificationsAfterSilent.some((n) => notificationText(n).includes(silentMessage))).toBe(false);
|
|
},
|
|
);
|
|
});
|