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 Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {duration, expect, test, testConfig} from '@mattermost/playwright-lib';
async function postToWebhook(webhookId: string, payload: Record<string, unknown>) {
const response = await fetch(`${testConfig.baseURL}/hooks/${webhookId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(duration.ten_sec),
});
if (!response.ok) {
throw new Error(`Webhook POST failed: ${response.status} ${await response.text()}`);
}
}
/**
* @objective Verify interactive attachment buttons remain visible across Indigo, Onyx, and Denim themes.
*/
test(
'MM-T5672 displays attachment buttons correctly across premade themes',
{tag: '@message_attachments'},
async ({pw}) => {
// # Post an attachment containing primary, danger, and default buttons
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'town-square');
const webhook = await adminClient.createIncomingWebhook({
channel_id: channel.id,
display_name: 'Theme buttons',
});
await postToWebhook(webhook.id, {
attachments: [
{
text: 'Theme button test',
actions: [
{id: 'primary', name: 'Primary action', type: 'button', style: 'primary'},
{id: 'danger', name: 'Danger action', type: 'button', style: 'danger'},
{id: 'default', name: 'Default action', type: 'button'},
],
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
const post = await channelsPage.getLastPost();
for (const theme of ['Indigo', 'Onyx', 'Denim'] as const) {
// # Select and save the premade theme
const settingsModal = await channelsPage.openSettings();
const displaySettings = await settingsModal.openDisplayTab();
await displaySettings.selectPremadeTheme(theme);
await settingsModal.close();
// * Verify all attachment buttons remain visible
await expect(post.container.getByRole('button', {name: 'Primary action'})).toBeVisible();
await expect(post.container.getByRole('button', {name: 'Danger action'})).toBeVisible();
await expect(post.container.getByRole('button', {name: 'Default action'})).toBeVisible();
}
},
);

View File

@@ -0,0 +1,306 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Locator} from '@playwright/test';
import {expect, test, testConfig} from '@mattermost/playwright-lib';
/**
* Posts a JSON payload to an incoming webhook URL.
*/
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()}`);
}
}
/** Legacy attachments are translated to mm_blocks; author/title live in markdown text blocks. */
function mmBlocks(lastPost: {container: Locator}) {
return lastPost.container.locator('.mm-blocks');
}
async function expectMmBlocksAuthorName(lastPost: {container: Locator}, name: string) {
const blocks = mmBlocks(lastPost);
// Attachment translation renders author before title; title <p> also contains the link text via hasText.
await expect(blocks.locator('p').first()).toHaveText(name);
}
async function expectMmBlocksTitleLink(lastPost: {container: Locator}, title: string) {
await expect(mmBlocks(lastPost).getByRole('link', {name: title})).toBeVisible();
}
test.describe('Message attachment special character decoding', () => {
/**
* @objective Verify that HTML entities in message attachment author_name and title
* fields are decoded to their readable characters when rendered in the UI.
* This ensures parity with the server-side Go html.UnescapeString behavior.
*/
test(
'decodes HTML entities in attachment title and author_name from an incoming webhook',
{tag: ['@smoke', '@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
// # Create an incoming webhook for the channel
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Attachment Test Hook',
});
// # Post a webhook payload with HTML-encoded entities in title and author_name
await postToWebhook(webhook.id, {
attachments: [
{
author_name: 'Bot &#40;v2.1&#41; &amp; Integrations',
title: 'Future Plan for Plugins &#40;3rd party &amp; core&#41;',
title_link: 'https://example.com',
text: 'Attachment body text',
},
],
});
// # Log in and navigate to the channel
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Get the last post which should contain the webhook attachment
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Verify the author name has decoded entities: &#40; → (, &#41; → ), &amp; → &
await expectMmBlocksAuthorName(lastPost, 'Bot (v2.1) & Integrations');
// * Verify the title has decoded entities
await expectMmBlocksTitleLink(lastPost, 'Future Plan for Plugins (3rd party & core)');
},
);
test(
'decodes numeric HTML entities in attachment title with a title_link',
{tag: ['@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Numeric Entities Hook',
});
// # Post with numeric entities: &#34; ("), &#39; ('), &#58; (:), &#91; ([), &#93; (])
await postToWebhook(webhook.id, {
attachments: [
{
title: '&#34;All Hands&#34; Meeting &#91;Q1&#93; &#45; 9&#58;00',
title_link: 'https://example.com',
text: 'Some body text',
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Verify the numeric entities are decoded in the title link
await expectMmBlocksTitleLink(lastPost, '"All Hands" Meeting [Q1] - 9:00');
},
);
test(
'decodes named HTML entities (&lt; &gt; &quot; &apos;) in author_name',
{tag: ['@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Named Entities Hook',
});
await postToWebhook(webhook.id, {
attachments: [
{
author_name: 'CI &lt;Build&gt; &quot;System&quot; &apos;Owner&apos;',
text: 'Build results',
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Verify named entities are decoded in the author name
await expectMmBlocksAuthorName(lastPost, 'CI <Build> "System" \'Owner\'');
},
);
test(
'does not double-decode already-encoded entities in attachment fields',
{tag: ['@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Double Encode Hook',
});
// &amp;lt; should decode only one level: &amp; → &, leaving &lt; as literal text
await postToWebhook(webhook.id, {
attachments: [
{
author_name: '&amp;lt;safe&amp;gt;',
title: '&amp;lt;safe&amp;gt;',
title_link: 'https://example.com',
text: 'Testing double-encoding safety',
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Should show &lt;safe&gt; literally, not <safe>
await expectMmBlocksAuthorName(lastPost, '&lt;safe&gt;');
await expectMmBlocksTitleLink(lastPost, '&lt;safe&gt;');
},
);
test(
'renders plain text without entities unchanged in attachment title and author_name',
{tag: ['@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Plain Text Hook',
});
await postToWebhook(webhook.id, {
attachments: [
{
author_name: 'Simple Bot Name',
title: 'A normal title with no special chars',
title_link: 'https://example.com',
text: 'Regular body',
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Verify text passes through unchanged
await expectMmBlocksAuthorName(lastPost, 'Simple Bot Name');
await expectMmBlocksTitleLink(lastPost, 'A normal title with no special chars');
},
);
test(
'decodes realistic calendar plugin payload with parentheses and ampersands',
{tag: ['@message_attachments']},
async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const channels = await adminClient.getMyChannels(team.id);
const townSquare = channels.find((ch) => ch.name === 'town-square');
if (!townSquare) {
throw new Error('Town Square channel not found');
}
const webhook = await adminClient.createIncomingWebhook({
channel_id: townSquare.id,
display_name: 'E2E Calendar Hook',
});
// Realistic payload from a Google Calendar-like plugin
await postToWebhook(webhook.id, {
attachments: [
{
author_name: 'Google Calendar &#124; via Plugin &#40;v1.0&#41;',
title: 'Team Standup &#40;Daily&#41; &#45; 9&#58;00 AM',
title_link: 'https://calendar.example.com/event/123',
text: 'You have an upcoming event',
},
],
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
const lastPost = await channelsPage.getLastPost();
await lastPost.toBeVisible();
await expect(mmBlocks(lastPost)).toBeVisible();
// * Verify realistic plugin content is properly decoded
await expectMmBlocksAuthorName(lastPost, 'Google Calendar | via Plugin (v1.0)');
await expectMmBlocksTitleLink(lastPost, 'Team Standup (Daily) - 9:00 AM');
},
);
});