138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
|
|
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();
|
||
|
|
});
|
||
|
|
});
|