Files
upstream-mattermost/e2e-tests/playwright/specs/functional/channels/channel_attributes/helpers.ts
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

213 lines
7.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Client4} from '@mattermost/client';
import type {Channel} from '@mattermost/types/channels';
import type {PropertyField, PropertyValue} from '@mattermost/types/properties';
import type {Team} from '@mattermost/types/teams';
// Channel attributes share the access_control group with Classification Markings.
export const GROUP = 'access_control';
export const TARGET_TYPE = 'system';
// Prefix on every field these specs create, so cleanup never touches real ones.
export const FIELD_PREFIX = 'chanattr_e2e';
// Mirrors the allow-list in model/property_field_attrs_validation.go.
export const DISPLAY_LABEL_HEADER = 'display_label_header';
export const DISPLAY_LABEL_INFO = 'display_label_info';
export const DISPLAY_BANNER_TOP = 'display_banner_top';
type CreateOptions = {
objectType?: 'channel' | 'user';
type?: 'select' | 'multiselect' | 'text';
options?: string[];
required?: boolean;
// Omit to leave unset, which reads as editable. Pass false to lock.
editable?: boolean;
// Undesignated attributes are stored but never shown.
actions?: string[];
// Defaults to member, so an ordinary channel member can set a value.
permissionValues?: 'none' | 'sysadmin' | 'admin' | 'member';
sortOrder?: number;
optionColors?: Record<string, string>;
};
// The server validates field names as CEL identifiers, so anything that is not
// a word character has to be folded away before it reaches the API.
export function attributeName(suffix: string, uniqueId: string): string {
return `${FIELD_PREFIX}_${suffix}_${uniqueId}`.replace(/[^A-Za-z0-9_]/g, '_');
}
export async function createAttribute(
adminClient: Client4,
name: string,
{
objectType = 'channel',
type,
options = [],
required,
editable,
actions,
permissionValues = 'member',
sortOrder,
optionColors,
}: CreateOptions = {},
): Promise<PropertyField> {
const field: Record<string, unknown> = {
name,
type: type ?? (options.length ? 'select' : 'text'),
target_type: TARGET_TYPE,
target_id: '',
permission_field: 'admin',
permission_values: permissionValues,
permission_options: 'admin',
};
const attrs: Record<string, unknown> = {};
if (options.length) {
attrs.options = options.map((optionName) => ({
id: '',
name: optionName,
...(optionColors?.[optionName] ? {color: optionColors[optionName]} : {}),
}));
}
if (required !== undefined) {
attrs.required = required;
}
if (editable !== undefined) {
attrs.editable = editable;
}
if (actions) {
attrs.actions = actions;
}
if (sortOrder !== undefined) {
attrs.sort_order = sortOrder;
}
if (Object.keys(attrs).length) {
field.attrs = attrs;
}
return adminClient.createPropertyField(GROUP, objectType, field as Parameters<Client4['createPropertyField']>[2]);
}
// Sets up channel state without driving the UI.
export async function setChannelValue(
client: Client4,
channelId: string,
field: PropertyField,
value: unknown,
): Promise<void> {
await client.patchPropertyValues(GROUP, 'channel', channelId, [
{field_id: field.id, value} as Parameters<Client4['patchPropertyValues']>[3][number],
]);
}
export async function createChannelForAttributes(
adminClient: Client4,
team: Team,
suffix: string,
displayName = `Attr ${suffix}`,
propertyValues?: Array<{field_id: string; value: unknown}>,
): Promise<Channel> {
return adminClient.createChannel({
team_id: team.id,
name: `attr-${suffix}`.toLowerCase(),
display_name: displayName,
type: 'O',
// Required attributes are refused server-side, so a channel seeded for a
// spec has to carry them in the create call.
...(propertyValues?.length ? {property_values: propertyValues} : {}),
} as Channel);
}
export async function setBannerTemplate(
client: Client4,
channelId: string,
text: string,
backgroundColor = '#1E325C',
): Promise<void> {
await client.patchChannel(channelId, {
banner_info: {enabled: true, text, background_color: backgroundColor},
} as Partial<Channel>);
}
// Mirrors attributeToken() in components/channel_attributes/banner_template.ts.
export function attributeToken(name: string): string {
return `{{${name}}}`;
}
export function optionId(field: PropertyField, name: string): string {
const options = (field.attrs?.options ?? []) as Array<{id: string; name: string}>;
const match = options.find((option) => option.name === name);
if (!match) {
throw new Error(`option ${name} not found on ${field.name}`);
}
return match.id;
}
// Values are typed loosely because a multiselect stores an array where a select
// stores a single option id. The endpoint answers a bare null, not an empty
// array, when a channel has no values at all.
export async function readChannelValues(client: Client4, channelId: string): Promise<Array<PropertyValue<unknown>>> {
const values = await client.getPropertyValues<unknown>(GROUP, 'channel', channelId);
return values ?? [];
}
export function valueFor(values: Array<PropertyValue<unknown>>, field: PropertyField): unknown {
return values.find((value) => value.field_id === field.id)?.value;
}
export async function deleteAttributes(adminClient: Client4, fields: PropertyField[]): Promise<void> {
for (const field of fields) {
try {
await adminClient.deletePropertyField(GROUP, field.object_type, field.id);
} catch {} // eslint-disable-line no-empty
}
}
// Best-effort cleanup of fields left behind by an interrupted run.
export async function purgeAttributes(adminClient: Client4): Promise<void> {
for (const objectType of ['channel', 'user'] as const) {
try {
const fields = await adminClient.getPropertyFields(GROUP, objectType, TARGET_TYPE);
const stale = (fields ?? []).filter(
(field) => field.name.startsWith(FIELD_PREFIX) && field.delete_at === 0,
);
await deleteAttributes(adminClient, stale);
} catch {} // eslint-disable-line no-empty
}
}
/**
* Throws when the server carries a required channel attribute these specs did not
* create.
*
* A stray one breaks every fixture that creates a channel — as a 400 from the API,
* as a 30s timeout on a disabled Create button here — and neither failure names the
* cause. purgeAttributes cannot clear them: it is scoped to FIELD_PREFIX so it never
* deletes a field someone meant to keep.
*/
export async function assertNoForeignRequiredAttributes(adminClient: Client4): Promise<void> {
const fields = await adminClient.getPropertyFields(GROUP, 'channel', TARGET_TYPE, undefined, {perPage: 200});
const foreign = (fields ?? []).filter(
(field) => field.delete_at === 0 && field.attrs?.required === true && !field.name.startsWith(FIELD_PREFIX),
);
if (foreign.length) {
const names = foreign.map((field) => field.name).join(', ');
throw new Error(
`Required channel attributes not owned by these specs are present: ${names}. ` +
'Channel creation cannot complete until each has a value. Delete them, or turn off Required, and re-run.',
);
}
}