* chore: bump version to 2.23.2 and enhance team account creation - Updated application version from 2.23.1 to 2.23.2 in package.json. - Enhanced team account creation to support slugs for non-Latin names, including validation and UI updates. - Updated localization files to reflect new slug requirements and error messages. - Refactored related schemas and server actions to accommodate slug handling in team account creation and updates. * refactor: remove old trigger and function for adding current user to new account - Dropped the trigger "add_current_user_to_new_account" and the associated function from the database schema. - Updated permissions for the function public.create_team_account to ensure proper access control.
357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
import { Page, expect, test } from '@playwright/test';
|
|
|
|
import { InvitationsPageObject } from '../invitations/invitations.po';
|
|
import { TeamAccountsPageObject } from './team-accounts.po';
|
|
|
|
// Helper function to set up a team with a member
|
|
async function setupTeamWithMember(page: Page, memberRole = 'member') {
|
|
// Setup invitations page object
|
|
const invitations = new InvitationsPageObject(page);
|
|
const teamAccounts = invitations.teamAccounts;
|
|
|
|
// Setup team with owner
|
|
const { email: ownerEmail, slug } = await invitations.setup();
|
|
|
|
// Navigate to members page
|
|
await invitations.navigateToMembers();
|
|
|
|
// Create a new member email and invite them with the specified role
|
|
const memberEmail = invitations.auth.createRandomEmail();
|
|
|
|
const invites = [
|
|
{
|
|
email: memberEmail,
|
|
role: memberRole,
|
|
},
|
|
];
|
|
|
|
await invitations.openInviteForm();
|
|
await invitations.inviteMembers(invites);
|
|
|
|
// Verify the invitation was sent
|
|
await expect(invitations.getInvitations()).toHaveCount(1);
|
|
|
|
// Sign out the current user
|
|
await page.context().clearCookies();
|
|
|
|
// Sign up with the new member email and accept the invitation
|
|
await invitations.auth.visitConfirmEmailLink(memberEmail);
|
|
|
|
await invitations.acceptInvitation();
|
|
|
|
await invitations.teamAccounts.openAccountsSelector();
|
|
|
|
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
|
|
|
|
// Sign out and sign back in as the original owner
|
|
await page.context().clearCookies();
|
|
|
|
await page.goto('/auth/sign-in');
|
|
|
|
await invitations.auth.loginAsUser({
|
|
email: ownerEmail,
|
|
next: '/home',
|
|
});
|
|
|
|
// Navigate to the team members page
|
|
await page.goto(`/home/${slug}/members`);
|
|
|
|
return { invitations, teamAccounts, ownerEmail, memberEmail, slug };
|
|
}
|
|
|
|
test.describe('Team Accounts', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
await teamAccounts.setup();
|
|
});
|
|
|
|
test('user can update their team name', async ({ page }) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
const newTeamName = `Updated-Team-${(Math.random() * 100000000).toFixed(0)}`;
|
|
|
|
await teamAccounts.goToSettings();
|
|
|
|
// Update just the name (slug stays the same for Latin names)
|
|
await teamAccounts.updateTeamName(newTeamName);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
await teamAccounts.openAccountsSelector();
|
|
|
|
await expect(teamAccounts.getTeamFromSelector(newTeamName)).toBeVisible();
|
|
});
|
|
|
|
test('cannot create a Team account using reserved names', async ({
|
|
page,
|
|
}) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
await teamAccounts.createTeam();
|
|
|
|
await teamAccounts.openAccountsSelector();
|
|
await page.click('[data-test="create-team-account-trigger"]');
|
|
|
|
await teamAccounts.tryCreateTeam('billing');
|
|
|
|
await expect(
|
|
page.getByText('This name is reserved. Please choose a different one.'),
|
|
).toBeVisible();
|
|
|
|
await teamAccounts.tryCreateTeam('settings');
|
|
|
|
await expect(
|
|
page.getByText('This name is reserved. Please choose a different one.'),
|
|
).toBeVisible();
|
|
|
|
function expectError() {
|
|
return expect(
|
|
page.getByText(
|
|
'This name cannot contain special characters. Please choose a different one.',
|
|
),
|
|
).toBeVisible();
|
|
}
|
|
|
|
await teamAccounts.tryCreateTeam('Test-Name#');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test,Name');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name/');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name\\');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name:');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name;');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name=');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name>');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name<');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name?');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name@');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name^');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name&');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name*');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name(');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name)');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name+');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name%');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name$');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name[');
|
|
await expectError();
|
|
|
|
await teamAccounts.tryCreateTeam('Test Name]');
|
|
await expectError();
|
|
});
|
|
|
|
test('can create a Team account with non-Latin name when providing a slug', async ({
|
|
page,
|
|
}) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
await teamAccounts.createTeam();
|
|
|
|
const random = (Math.random() * 100000000).toFixed(0);
|
|
const slug = `korean-team-${random}`;
|
|
|
|
// Create team with Korean name
|
|
await teamAccounts.createTeamWithNonLatinName('한국 팀', slug);
|
|
|
|
// Verify we're on the team page
|
|
await expect(page).toHaveURL(`/home/${slug}`);
|
|
|
|
// Verify team appears in selector
|
|
await teamAccounts.openAccountsSelector();
|
|
await expect(teamAccounts.getTeamFromSelector('한국 팀')).toBeVisible();
|
|
});
|
|
|
|
test('slug validation shows error for invalid characters', async ({
|
|
page,
|
|
}) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
await teamAccounts.createTeam();
|
|
|
|
// Use non-Latin name to trigger the slug field visibility
|
|
await teamAccounts.openAccountsSelector();
|
|
await page.click('[data-test="create-team-account-trigger"]');
|
|
|
|
await page.fill(
|
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
|
'テストチーム',
|
|
);
|
|
|
|
// Wait for slug field to appear (triggered by non-Latin name)
|
|
await expect(teamAccounts.getSlugField()).toBeVisible();
|
|
|
|
// Test invalid slug with uppercase
|
|
await page.fill(
|
|
'[data-test="create-team-form"] [data-test="team-slug-input"]',
|
|
'Invalid-Slug',
|
|
);
|
|
|
|
await page.click('[data-test="create-team-form"] button:last-child');
|
|
|
|
await expect(
|
|
page.getByText(
|
|
'Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed',
|
|
{ exact: true },
|
|
),
|
|
).toBeVisible();
|
|
|
|
// Test invalid slug with non-Latin characters
|
|
await page.fill(
|
|
'[data-test="create-team-form"] [data-test="team-slug-input"]',
|
|
'тест-slug',
|
|
);
|
|
|
|
await page.click('[data-test="create-team-form"] button:last-child');
|
|
|
|
await expect(
|
|
page.getByText(
|
|
'Only English letters (a-z), numbers (0-9), and hyphens (-) are allowed',
|
|
{ exact: true },
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Team Account Deletion', () => {
|
|
test('user can delete their team account', async ({ page }) => {
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
const params = teamAccounts.createTeamName();
|
|
|
|
const { email } = await teamAccounts.setup(params);
|
|
await teamAccounts.goToSettings();
|
|
|
|
await teamAccounts.deleteAccount(email);
|
|
await teamAccounts.openAccountsSelector();
|
|
|
|
await expect(
|
|
teamAccounts.getTeamFromSelector(params.teamName),
|
|
).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Team Member Role Management', () => {
|
|
test("owner can update a team member's role", async ({ page }) => {
|
|
// Setup team with a regular member
|
|
const { teamAccounts, memberEmail } = await setupTeamWithMember(page);
|
|
|
|
// Get the current role badge text
|
|
const memberRow = page.getByRole('row', { name: memberEmail });
|
|
|
|
const initialRoleBadge = memberRow.locator(
|
|
'[data-test="member-role-badge"]',
|
|
);
|
|
|
|
await expect(initialRoleBadge).toHaveText('Member');
|
|
|
|
// Update the member's role to admin
|
|
await teamAccounts.updateMemberRole(memberEmail, 'owner');
|
|
|
|
await expect(
|
|
page
|
|
.getByRole('row', { name: memberEmail })
|
|
.locator('[data-test="member-role-badge"]'),
|
|
).toHaveText('Owner');
|
|
});
|
|
});
|
|
|
|
test.describe('Team Ownership Transfer', () => {
|
|
test('owner can transfer ownership to another team member', async ({
|
|
page,
|
|
}) => {
|
|
// Setup team with an owner member (required for ownership transfer)
|
|
const { teamAccounts, ownerEmail, memberEmail } = await setupTeamWithMember(
|
|
page,
|
|
'owner',
|
|
);
|
|
|
|
// Transfer ownership to the member
|
|
await teamAccounts.transferOwnership(memberEmail, ownerEmail);
|
|
|
|
// Verify the transfer was successful by checking if the primary owner badge
|
|
// is now on the new owner's row
|
|
const memberRow = page.getByRole('row', { name: memberEmail });
|
|
|
|
// Check for the primary owner badge on the member's row
|
|
await expect(memberRow.locator('text=Primary Owner')).toBeVisible();
|
|
|
|
// The original owner should no longer have the primary owner badge
|
|
const ownerRow = page.getByRole('row', { name: ownerEmail.split('@')[0] });
|
|
await expect(ownerRow.locator('text=Primary Owner')).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Team Account Security', () => {
|
|
test('unauthorized user cannot access team account', async ({
|
|
page,
|
|
browser,
|
|
}) => {
|
|
// 1. Create a team account with User A
|
|
const teamAccounts = new TeamAccountsPageObject(page);
|
|
const params = teamAccounts.createTeamName();
|
|
|
|
// Setup User A and create team
|
|
await teamAccounts.setup(params);
|
|
|
|
// Store team slug for later use
|
|
const teamSlug = params.slug;
|
|
|
|
// 2. Sign out User A
|
|
await page.context().clearCookies();
|
|
|
|
// 3. Create a new context for User B (to have clean cookies/session)
|
|
const userBContext = await browser.newContext();
|
|
const userBPage = await userBContext.newPage();
|
|
const userBTeamAccounts = new TeamAccountsPageObject(userBPage);
|
|
|
|
// Sign up with User B
|
|
await userBPage.goto('/auth/sign-up');
|
|
const emailB = userBTeamAccounts.auth.createRandomEmail();
|
|
|
|
await userBTeamAccounts.auth.signUp({
|
|
email: emailB,
|
|
password: 'password',
|
|
repeatPassword: 'password',
|
|
});
|
|
|
|
await userBTeamAccounts.auth.visitConfirmEmailLink(emailB);
|
|
|
|
// 4. Attempt to access the team page with User B
|
|
await userBPage.goto(`/home/${teamSlug}`);
|
|
|
|
// Check that we're not on the team page anymore (should redirect)
|
|
await expect(userBPage).toHaveURL(`/home`);
|
|
});
|
|
});
|