diff --git a/.windsurfrules b/.windsurfrules index 32b435d0c..70f97ace5 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -39,3 +39,4 @@ t("menu", { defaultValue: "Menu" }); 27. If the i18nKey is not intended to be reused, prefix it with the component name in camelCase 28. Always use kebab-case for file names 29. Prefer double quotes for strings over single quotes +30. Only add comments when it is necessary to explain code that isn't self-explanatory \ No newline at end of file diff --git a/apps/web/.env.test b/apps/web/.env.test index d5d544883..1c65a0718 100644 --- a/apps/web/.env.test +++ b/apps/web/.env.test @@ -8,3 +8,4 @@ SMTP_HOST=0.0.0.0 SMTP_PORT=1025 QUICK_CREATE_ENABLED=true API_SECRET=1234567890abcdef1234567890abcdef1234 +INITIAL_ADMIN_EMAIL=initial.admin@rallly.co diff --git a/apps/web/tests/admin-setup.spec.ts b/apps/web/tests/admin-setup.spec.ts new file mode 100644 index 000000000..f9bedc795 --- /dev/null +++ b/apps/web/tests/admin-setup.spec.ts @@ -0,0 +1,106 @@ +import { expect, test } from "@playwright/test"; +import { prisma } from "@rallly/database"; +import { deleteAllMessages } from "./mailpit/mailpit"; +import { createUserInDb, loginWithEmail } from "./test-utils"; + +const INITIAL_ADMIN_TEST_EMAIL = "initial.admin@rallly.co"; +const REGULAR_USER_EMAIL = "user@example.com"; +const SUBSEQUENT_ADMIN_EMAIL = "admin2@example.com"; +const OTHER_USER_EMAIL = "other.user@example.com"; + +test.describe("Admin Setup Page Access", () => { + test.beforeEach(async () => { + await prisma.user.deleteMany({ + where: { + email: { + in: [ + INITIAL_ADMIN_TEST_EMAIL, + REGULAR_USER_EMAIL, + SUBSEQUENT_ADMIN_EMAIL, + OTHER_USER_EMAIL, + ], + }, + }, + }); + + await deleteAllMessages(); + }); + + test("should redirect unauthenticated user to login page", async ({ + page, + }) => { + await page.goto("/admin-setup"); + await expect(page).toHaveURL(/.*\/login/); + }); + + test("should allow access if user is the designated initial admin (and not yet admin role)", async ({ + page, + }) => { + await createUserInDb( + INITIAL_ADMIN_TEST_EMAIL, + "Initial Admin User", + "user", + ); + await loginWithEmail(page, INITIAL_ADMIN_TEST_EMAIL); + + await page.goto("/admin-setup"); + await expect(page).toHaveURL(/.*\/admin-setup/); + await expect(page.getByText("Are you the admin?")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Make me an admin" }), + ).toBeVisible(); + }); + + test("should show 'not found' for a regular user (not initial admin, not admin role)", async ({ + page, + }) => { + await createUserInDb(REGULAR_USER_EMAIL, "Regular User", "user"); + await loginWithEmail(page, REGULAR_USER_EMAIL); + + await page.goto("/admin-setup"); + await expect(page.getByText("404 not found")).toBeVisible(); + }); + + test("should redirect an existing admin user to control-panel", async ({ + page, + }) => { + await createUserInDb(SUBSEQUENT_ADMIN_EMAIL, "Existing Admin", "admin"); + await loginWithEmail(page, SUBSEQUENT_ADMIN_EMAIL); + + await page.goto("/admin-setup"); + await expect(page).toHaveURL(/.*\/control-panel/); + }); + + test("should show 'not found' if INITIAL_ADMIN_EMAIL in env is different from user's email", async ({ + page, + }) => { + await createUserInDb(OTHER_USER_EMAIL, "Other User", "user"); + await loginWithEmail(page, OTHER_USER_EMAIL); + + await page.goto("/admin-setup"); + await expect(page.getByText("404 not found")).toBeVisible(); + }); + + test("initial admin can make themselves admin using the button", async ({ + page, + }) => { + await createUserInDb( + INITIAL_ADMIN_TEST_EMAIL, + "Initial Admin To Be", + "user", + ); + await loginWithEmail(page, INITIAL_ADMIN_TEST_EMAIL); + + await page.goto("/admin-setup"); + await expect(page.getByText("Are you the admin?")).toBeVisible(); + await page.getByRole("button", { name: "Make me an admin" }).click(); + + await expect(page).toHaveURL(/.*\/control-panel/, { timeout: 10000 }); + + const user = await prisma.user.findUnique({ + where: { email: INITIAL_ADMIN_TEST_EMAIL }, + }); + expect(user).toBeTruthy(); + expect(user?.role).toBe("admin"); + }); +}); diff --git a/apps/web/tests/login-page.ts b/apps/web/tests/login-page.ts index f5d77444b..f4467cd9a 100644 --- a/apps/web/tests/login-page.ts +++ b/apps/web/tests/login-page.ts @@ -22,5 +22,8 @@ export class LoginPage { const code = await getCode(email); await this.page.getByText("Finish Logging In").waitFor(); await this.page.getByPlaceholder("Enter your 6-digit code").fill(code); + + // Wait for page to load + await this.page.waitForLoadState("networkidle"); } } diff --git a/apps/web/tests/test-utils.ts b/apps/web/tests/test-utils.ts new file mode 100644 index 000000000..2176fa04e --- /dev/null +++ b/apps/web/tests/test-utils.ts @@ -0,0 +1,28 @@ +import type { Page } from "@playwright/test"; +import { prisma } from "@rallly/database"; +import { LoginPage } from "./login-page"; + +export async function createUserInDb( + email: string, + name: string, + role: "user" | "admin" = "user", +) { + return prisma.user.create({ + data: { + email, + name, + role, + locale: "en", + timeZone: "Europe/London", + emailVerified: new Date(), + }, + }); +} + +export async function loginWithEmail(page: Page, email: string) { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login({ + email, + }); +}