Files
zeekayandHanzo Dev c85589eb27 serve the API at /v1/workspace
The base moves off /v1/team for two reasons that point the same way.

It collided: /v1/team on hanzo.team is a carve-out routed to the older
cloud service, so the moment this image shipped its own API would have
been answered by something else -- a 404 on our own endpoints, on the
host we serve from.

And workspace is the better noun. It is what the product calls itself
in its own words, and it does not read as though the API belongs to one
team among several.

No /api/ anywhere in a path we own.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 16:03:09 -07:00

462 lines
20 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, getRandomId, test} from '@mattermost/playwright-lib';
test.describe('Single-channel guests', () => {
test.setTimeout(180000);
test.describe.configure({mode: 'serial'});
test.beforeEach(async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
});
async function setupSingleChannelGuestsTest(pw: any) {
const {adminClient, adminUser} = await pw.getAdminClient();
const suffix = getRandomId();
const team = await adminClient.createTeam({
name: `scg-${suffix}`,
display_name: `SCG ${suffix}`,
type: 'O',
});
await adminClient.addToTeam(team.id, adminUser.id);
return {adminClient, adminUser, team};
}
async function patchGuestEnabled(adminClient: any, enabled: boolean): Promise<boolean> {
const cfg = await adminClient.getConfig();
const previous = cfg.GuestAccountsSettings?.Enable ?? false;
await adminClient.patchConfig({GuestAccountsSettings: {Enable: enabled}});
return previous;
}
async function navigateWithGuestPatch(page: any, adminClient: any, url: string, guestEnabled: boolean) {
await page.goto(url);
await page.waitForLoadState('networkidle');
await patchGuestEnabled(adminClient, guestEnabled);
await page.reload();
await page.waitForLoadState('networkidle');
}
/**
* @objective Verify the Single-channel Guests stat card appears on the Site Statistics page when guests are enabled
*
* @precondition
* Server has a non-Entry license with guest accounts enabled
*/
test(
'displays single-channel guests card on site statistics page when guest accounts are enabled',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient, team} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Create a guest user and add to one channel
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
await adminClient.addToTeam(team.id, guestUser.id);
const channel = await adminClient.createChannel(
pw.random.channel({teamId: team.id, name: 'guest-channel', displayName: 'Guest Channel', unique: true}),
);
await adminClient.addToChannel(guestUser.id, channel.id);
// # Log in as admin and navigate to site statistics
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// Re-apply patch after initial load + reload to counter WebSocket config resets
// from concurrent initSetup() calls (default_config has Enable: false).
await navigateWithGuestPatch(
systemConsolePage.page,
adminClient,
'/admin_console/reporting/system_analytics',
true,
);
// * Verify the single-channel guests card is visible
await expect(systemConsolePage.page.getByTestId('singleChannelGuests')).toBeVisible({timeout: 30000});
// * Verify the count is at least 1.
// Analytics are indexed asynchronously on the server — poll with reload until
// the card reflects the newly created guest (can take several seconds in CI).
await expect
.poll(
async () => {
// Re-apply guest patch before reload: a concurrent initSetup() may
// reset the config back to the default (Enable: false), which would
// hide the card and make the textContent call return null.
await patchGuestEnabled(adminClient, true);
await systemConsolePage.page.reload();
await systemConsolePage.page.waitForLoadState('networkidle');
const text = await systemConsolePage.page
.getByTestId('singleChannelGuests')
.textContent()
.catch(() => '');
return Number(text?.match(/(\d+)/)?.[1] ?? 0);
},
{timeout: 180000, intervals: [3000, 5000, 8000, 12000]},
)
.toBeGreaterThanOrEqual(1);
},
);
/**
* @objective Verify the Single-channel Guests row appears on the Edition and License page when guests are enabled
*
* @precondition
* Server has a non-Entry license with guest accounts enabled and a single-channel guest limit configured
*/
test(
'displays single-channel guests row on edition and license page when guest accounts are enabled',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient, team} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Create a guest user and add to one channel
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
await adminClient.addToTeam(team.id, guestUser.id);
const channel = await adminClient.createChannel(
pw.random.channel({teamId: team.id, name: 'guest-channel', displayName: 'Guest Channel', unique: true}),
);
await adminClient.addToChannel(guestUser.id, channel.id);
// # Log in as admin and navigate to edition and license page
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// Re-apply patch after initial load + reload to counter WebSocket config resets.
await navigateWithGuestPatch(systemConsolePage.page, adminClient, '/admin_console/about/license', true);
// * Verify the single-channel guests row is visible
await expect(systemConsolePage.page.getByText('SINGLE-CHANNEL GUESTS:')).toBeVisible();
},
);
/**
* @objective Verify the Single-channel Guests stat card is not shown when guest accounts are disabled
*/
test(
'hides single-channel guests card on site statistics page when guest accounts are disabled',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Disable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, false);
// # Log in as admin and navigate to site statistics
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// Re-apply patch (disabled) + reload to ensure the browser reads the fresh config.
await navigateWithGuestPatch(
systemConsolePage.page,
adminClient,
'/admin_console/reporting/system_analytics',
false,
);
// * Verify the single-channel guests card is not in the DOM
await expect(systemConsolePage.page.getByTestId('singleChannelGuests')).not.toBeVisible();
},
);
/**
* @objective Verify the server limits API returns single-channel guest count and limit for admin users
*
* @precondition
* Server has a non-Entry license with guest accounts enabled
*/
test(
'returns single-channel guest data from server limits API for admin users',
{tag: '@system_console'},
async ({pw}) => {
const {adminClient} = await setupSingleChannelGuestsTest(pw);
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Fetch server limits
const {data: limits} = await adminClient.getServerLimits();
// * Verify the response includes single-channel guest fields
expect(limits).toHaveProperty('singleChannelGuestCount');
expect(limits).toHaveProperty('singleChannelGuestLimit');
expect(typeof limits.singleChannelGuestCount).toBe('number');
expect(typeof limits.singleChannelGuestLimit).toBe('number');
expect(limits.singleChannelGuestCount).toBeGreaterThanOrEqual(0);
expect(limits.singleChannelGuestLimit).toBeGreaterThanOrEqual(0);
},
);
/**
* @objective Verify the single-channel guests card does not show error styling when count is within limit
*
* @precondition
* Server has a non-Entry license with guest accounts enabled and guest count is within the allowed limit
*/
test(
'shows no error styling on single-channel guests card when count is within limit',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient, team} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Create a single-channel guest (count will be well within any license limit)
const guestUser = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guestUser.id, 'system_guest');
await adminClient.addToTeam(team.id, guestUser.id);
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
name: 'guest-no-overage',
displayName: 'Guest No Overage',
unique: true,
}),
);
await adminClient.addToChannel(guestUser.id, channel.id);
// # Navigate to site statistics
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
// Re-apply patch + reload to counter WebSocket config resets.
await navigateWithGuestPatch(
systemConsolePage.page,
adminClient,
'/admin_console/reporting/system_analytics',
true,
);
// * Verify the card title does NOT have error class (count is within limit)
const cardTitle = systemConsolePage.page.getByTestId('singleChannelGuestsTitle');
await expect(cardTitle).toBeVisible({timeout: 15000});
await expect(cardTitle).not.toHaveClass(/team_statistics--error/);
},
);
/**
* @objective Verify the dismissible banner is not shown when single-channel guest count is within limit
*
* @precondition
* Server has a non-Entry license with guest accounts enabled and guest count is within the allowed limit
*/
test('does not show guest limit banner when count is within limit', {tag: '@system_console'}, async ({pw}) => {
const {adminUser, adminClient} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Navigate to system console and re-apply patch + reload so the browser reads
// the latest config (not a WebSocket-clobbered Redux store from a concurrent initSetup).
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
await patchGuestEnabled(adminClient, true);
await systemConsolePage.page.reload();
await systemConsolePage.page.waitForLoadState('networkidle');
// * Verify the guest limit banner is not visible (count is within limit)
await expect(systemConsolePage.page.getByTestId('single_channel_guest_limit_banner')).not.toBeVisible();
});
/**
* @objective Verify error styling appears on the single-channel guests card and dismissible banner shows when single-channel guest count exceeds the limit
*
* @precondition
* Server has a non-Entry license with guest accounts enabled
*/
test(
'shows error styling on guests card and banner when single-channel guest count exceeds limit',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient, team} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Create multiple single-channel guests so the analytics count exceeds the mocked limit
for (let i = 0; i < 3; i++) {
const guest = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(guest.id, 'system_guest');
await adminClient.addToTeam(team.id, guest.id);
const ch = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
name: `scg-overage-${i}`,
displayName: `SCG Overage ${i}`,
unique: true,
}),
);
await adminClient.addToChannel(guest.id, ch.id);
}
// # Log in as admin (new browser context + page).
const {context, systemConsolePage} = await pw.testBrowser.login(adminUser);
// # Mock GET /v1/workspace/limits/server on the *context* before any navigation so the first
// getServerLimits() populates Redux with a low limit. Also fulfill with an explicit
// JSON body — route.fulfill({response, json}) does not reliably merge bodies in Playwright.
await context.route('**/v1/workspace/limits/server', async (route) => {
const response = await route.fetch();
let json: Record<string, unknown> = {};
try {
json = (await response.json()) as Record<string, unknown>;
} catch {
// ignore — replace with minimal payload
}
json.singleChannelGuestLimit = 1;
json.singleChannelGuestCount = 3;
await route.fulfill({
status: response.status(),
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(json),
});
});
// # Navigate to site statistics page, re-applying patch to counter concurrent resets.
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
await navigateWithGuestPatch(
systemConsolePage.page,
adminClient,
'/admin_console/reporting/system_analytics',
true,
);
// * Verify the card title has error styling
const cardTitle = systemConsolePage.page.getByTestId('singleChannelGuestsTitle');
await expect(cardTitle).toBeVisible({timeout: 15000});
await expect(cardTitle).toHaveClass(/team_statistics--error/);
// * Verify the dismissible guest limit banner is visible
await expect(systemConsolePage.page.getByTestId('single_channel_guest_limit_banner')).toBeVisible();
},
);
/**
* @objective Verify that a guest in multiple channels is not counted as a single-channel guest
*
* @precondition
* Server has a non-Entry license with guest accounts enabled
*/
test(
'does not count multi-channel guest as single-channel guest on site statistics page',
{tag: '@system_console'},
async ({pw}) => {
const {adminUser, adminClient, team} = await setupSingleChannelGuestsTest(pw);
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Enable guest accounts (narrow patch, not destructive full-config update)
await patchGuestEnabled(adminClient, true);
// # Create a guest user and add to TWO channels
const multiChannelGuest = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(multiChannelGuest.id, 'system_guest');
await adminClient.addToTeam(team.id, multiChannelGuest.id);
const channelA = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
name: 'guest-channel-a',
displayName: 'Guest Channel A',
unique: true,
}),
);
const channelB = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
name: 'guest-channel-b',
displayName: 'Guest Channel B',
unique: true,
}),
);
await adminClient.addToChannel(multiChannelGuest.id, channelA.id);
await adminClient.addToChannel(multiChannelGuest.id, channelB.id);
// # Log in as admin and navigate to site statistics, re-applying patch to counter
// concurrent initSetup() resets (default_config has GuestAccountsSettings.Enable: false).
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await systemConsolePage.goto();
await systemConsolePage.toBeVisible();
await navigateWithGuestPatch(
systemConsolePage.page,
adminClient,
'/admin_console/reporting/system_analytics',
true,
);
// * Verify the single-channel guests card is visible
const singleChannelGuestsCard = systemConsolePage.page.getByTestId('singleChannelGuests');
await expect(singleChannelGuestsCard).toBeVisible({timeout: 15000});
// * Verify the count text is present — multi-channel guest should not increment it
const countText = await singleChannelGuestsCard.textContent();
const match = countText?.match(/(\d+)/);
expect(match).toBeTruthy();
const singleChannelGuestCount = Number(match![1]);
// # Now create a single-channel guest to confirm baseline counting works
const singleChannelGuest = await adminClient.createUser(await pw.random.user(), '', '');
await adminClient.updateUserRoles(singleChannelGuest.id, 'system_guest');
await adminClient.addToTeam(team.id, singleChannelGuest.id);
await adminClient.addToChannel(singleChannelGuest.id, channelA.id);
// # Reload page to get updated stats
await systemConsolePage.page.reload();
await systemConsolePage.page.waitForLoadState('networkidle');
// * Verify the count increased by exactly 1 for the new single-channel guest
const updatedCountText = await singleChannelGuestsCard.textContent();
const updatedMatch = updatedCountText?.match(/(\d+)/);
expect(updatedMatch).toBeTruthy();
expect(Number(updatedMatch![1])).toBe(singleChannelGuestCount + 1);
},
);
});