Files
upstream-mirror 240db4e2a7
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
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

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);
},
);
});