my-fullstack-ai-platform/e2e/invitation.spec.ts

138 lines
5.6 KiB
TypeScript
Raw Permalink Normal View History

import { test, expect } from '@playwright/test';
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || '';
const supabase = createClient(supabaseUrl, supabaseKey);
const ownerEmail = `owner_${Date.now()}@example.com`;
const ownerPassword = 'testpassword123';
const inviteeEmail = `invitee_${Date.now()}@example.com`;
const inviteePassword = 'testpassword123';
test.describe('Team Invitation Flow', () => {
test('Should process creating, revoking and accepting invitations', async ({ browser }) => {
// We use isolated browser contexts to simulate two users
const ownerContext = await browser.newContext();
const ownerPage = await ownerContext.newPage();
// Step 1: Sign up owner
await ownerPage.goto('/auth/sign-up');
await ownerPage.fill('input[type="email"]', ownerEmail);
await ownerPage.fill('input[id="password"]', ownerPassword);
await ownerPage.fill('input[id="repeat-password"]', ownerPassword);
await ownerPage.click('button[type="submit"]');
// NEW: Auto-confirm the owner email using Service Role Key
await ownerPage.waitForTimeout(1000);
const { data: { users: ownerUsers } } = await supabase.auth.admin.listUsers();
const ownerUser = ownerUsers.find(u => u.email === ownerEmail);
if (ownerUser) {
await supabase.auth.admin.updateUserById(ownerUser.id, { email_confirm: true });
}
// Wait for the auth redirect
await ownerPage.waitForURL('**/auth/sign-up-success**', { timeout: 10000 }).catch(() => null);
// Some implementations redirect to sign-up-success then require manual login
if (ownerPage.url().includes('sign-up-success')) {
await ownerPage.goto('/auth/login');
await ownerPage.fill('input[type="email"]', ownerEmail);
await ownerPage.fill('input[type="password"]', ownerPassword);
await ownerPage.click('button[type="submit"]');
}
// Onboarding
await ownerPage.waitForURL('**/onboarding');
await ownerPage.fill('input[name="name"]', 'Test E2E Organization');
await ownerPage.click('button[type="submit"]');
// Dashboard
await ownerPage.waitForURL('**/dashboard');
// Step 2: Go to Team Settings
await ownerPage.goto('/dashboard/settings/team');
// Send Invite to be revoked
await ownerPage.fill('input[name="email"]', 'revokeme@example.com');
await ownerPage.click('button:has-text("Send Invite")');
// Verify it appears in pending
await expect(ownerPage.locator(`text=revokeme@example.com`)).toBeVisible();
// Step 3: Revoke Invite
await ownerPage.locator('li').filter({ hasText: 'revokeme@example.com' }).locator('button[title="Revoke Invitation"]').click();
// Verify it's gone
await expect(ownerPage.locator(`text=revokeme@example.com`)).not.toBeVisible();
// Step 4: Create real invite
await ownerPage.fill('input[name="email"]', inviteeEmail);
await ownerPage.click('button:has-text("Send Invite")');
await expect(ownerPage.locator(`text=${inviteeEmail}`)).toBeVisible();
// Fetch the token directly from database
let token = '';
// Wait to ensure database has processed the insert
await ownerPage.waitForTimeout(2000);
const { data: invitations } = await supabase
.from('invitations')
.select('token')
.eq('email', inviteeEmail)
.eq('status', 'pending');
token = invitations?.[0]?.token;
expect(token).toBeTruthy();
// Step 5: Invitee flow
const inviteeContext = await browser.newContext();
const inviteePage = await inviteeContext.newPage();
// Access the invite link
await inviteePage.goto(`/api/invite/accept?token=${token}`);
// Should be redirected to sign up (if not logged in)
await inviteePage.waitForURL('**/auth/sign-up**');
// Verify email is pre-filled and locked
const emailInput = inviteePage.locator('input[type="email"]');
await expect(emailInput).toHaveValue(inviteeEmail);
await expect(emailInput).toHaveAttribute('readOnly', '');
// Sign up invitee
await inviteePage.fill('input[id="password"]', inviteePassword);
await inviteePage.fill('input[id="repeat-password"]', inviteePassword);
await inviteePage.click('button[type="submit"]');
// NEW: Auto-confirm the invitee email using Service Role Key
await inviteePage.waitForTimeout(1000);
const { data: { users: inviteeUsers } } = await supabase.auth.admin.listUsers();
const inviteeUser = inviteeUsers.find(u => u.email === inviteeEmail);
if (inviteeUser) {
await supabase.auth.admin.updateUserById(inviteeUser.id, { email_confirm: true });
}
// Login if needed
if (inviteePage.url().includes('sign-up-success')) {
await inviteePage.goto('/auth/login');
await inviteePage.fill('input[type="email"]', inviteeEmail);
await inviteePage.fill('input[type="password"]', inviteePassword);
await inviteePage.click('button[type="submit"]');
}
// Should automatically skip onboarding and land in dashboard
await inviteePage.waitForURL('**/dashboard');
// Step 6: Verify Invitee is in Team Settings
await inviteePage.goto('/dashboard/settings/team');
// Invitee should see themselves in Members list (and the owner)
await expect(inviteePage.locator('h2:has-text("Members")')).toBeVisible();
await expect(inviteePage.locator(`li`, { hasText: inviteeEmail })).toBeVisible();
await expect(inviteePage.locator(`li`, { hasText: ownerEmail })).toBeVisible();
});
});