import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; import { createId } from "@paralleldrive/cuid2"; import { expect, test, type BrowserContext, type Page } from "@playwright/test"; import "dotenv/config"; import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { Lucia, type Cookie } from "lucia"; import createClient from "openapi-fetch"; import postgres from "postgres"; import * as schema from "~/drizzle/schema"; import type * as discord from "~/types/discord"; import type * as liljudd from "~/types/liljudd"; const unencoded = `${process.env.DISCORD_CLIENT_ID}:${process.env.DISCORD_CLIENT_SECRET}`; const encoded = btoa(unencoded); const queryClient = postgres(process.env.DATABASE_URL!); const db = drizzle(queryClient, { schema, }); const adapter = new DrizzlePostgreSQLAdapter(db, schema.sessions, schema.users); export const lucia = new Lucia(adapter, { getUserAttributes: (attributes) => attributes, }); let context: BrowserContext; let page: Page; let sessionCookie: Cookie | undefined; let userId = createId(); let guildId: bigint; test.describe.serial("User auth process", () => { test.beforeAll(() => { expect( [ "DISCORD_CLIENT_ID", "DISCORD_CLIENT_SECRET", "DATABASE_URL", "DISCORD_BOT_TOKEN", ].filter((e) => typeof process.env[e] === "undefined").length, { message: "Please specify all env vars." }, ).toBeFalsy(); }); test.beforeAll(async ({ browser }) => { context = await browser.newContext(); page = await context.newPage(); }); test.beforeEach(async () => { if (!sessionCookie) return; const sameSiteProps = { lax: "Lax", strict: "Strict", none: "None", } as const; const expires = sessionCookie.attributes.expires ? sessionCookie.attributes.expires.getTime() / 1000 : undefined; const sameSite = sessionCookie.attributes.sameSite ? sameSiteProps[sessionCookie.attributes.sameSite] : undefined; await context.addCookies([ { name: sessionCookie.name, value: sessionCookie.value, ...sessionCookie.attributes, sameSite, expires, secure: false, domain: "localhost", path: "/", }, ]); }); test.afterAll("Delete DB entries", async () => { await db.delete(schema.users).where(eq(schema.users.id, userId)).execute(); await db .delete(schema.guilds) .where(eq(schema.guilds.id, guildId)) .execute(); }); test.afterAll(async () => { await context.close(); }); test("Landing page", async () => { await page.goto("/"); await page.waitForLoadState("load"); expect(await page.screenshot()).toMatchSnapshot("landing_page.png"); }); test("Unauthorized Access Redirect Test", async () => { await page.goto("/config"); await page.waitForURL("/"); }); test("Generate auth session for further tests", async ({ browser }) => { const { GET } = createClient({ baseUrl: "https://discord.com/api/v10", }); const discordUserResponse = await GET("/users/@me", { headers: { Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`, }, }); if (discordUserResponse.error) throw discordUserResponse.error; const discordUser = discordUserResponse.data; const browserName = browser.browserType().name() as | "chromium" | "webkit" | "firefox"; userId = discordUser.id + userId.slice(discordUser.id.length); userId = userId.slice(0, -browserName.length) + browserName; enum BrowserIds { chromium, webkit, firefox, } guildId = BigInt(discordUser.id) ^ BigInt(BrowserIds[browserName]); await db.insert(schema.users).values({ id: userId, discord_id: discordUser.id, name: discordUser.global_name, image: discordUser.avatar, }); const session = await lucia.createSession( userId, {}, { sessionId: createId() }, ); sessionCookie = lucia.createSessionCookie(session.id); await db .insert(schema.discordTokens) .values({ userId, accessToken: "tokens.accessToken", expiresAt: sessionCookie.attributes.expires ?? new Date(), refreshToken: "tokens.refreshToken", }) .returning() .execute(); }); test("Landing page when logged in", async () => { await page.goto("/"); await page.waitForLoadState("load"); expect(await page.screenshot()).toMatchSnapshot( "landing_page_logged_in.png", ); }); test("Test Api", async () => { const { GET, POST, PUT } = createClient({ baseUrl: "http://localhost:3000/", }); const createConfigResponse = await POST("/api/{guildId}/config", { params: { path: { guildId: guildId.toString(), }, }, headers: { Authorization: `Basic ${encoded}`, Origin: "http://localhost:3000", }, }); if (createConfigResponse.error) throw new Error(createConfigResponse.error.error); let getConfigResponse = await GET("/api/{guildId}/config", { params: { path: { guildId: guildId.toString(), }, }, headers: { Authorization: `Basic ${encoded}`, Origin: "http://localhost:3000", }, }); if (getConfigResponse.error) throw new Error(getConfigResponse.error.error); switch (getConfigResponse.data?.checksum) { case "9d9ba8fa6405653cb98a961c533ac7e92cbc3af6": // webkit case "cf6316140d481bd5c1728828b065efbe8f7bb537": // firefox case "9e608cb56e0818c83334389ab3913eade9c011f7": // chromium break; default: throw new Error( "Before guild GET checksum didn't matched known ones: " + getConfigResponse.data?.checksum, ); } const putTimePlanningResponse = await PUT("/api/{guildId}/timePlanning", { body: { channelId: "1234567890123456789", targetMinute: 1, targetHour: 2, targetDay: 3, roles: { enabled: true, isAvailableRoleId: "1234567890123456789", wantsToBeNotifieRoledId: "1234567890123456789", }, messageIds: { "0": "1234567890123456789", "1": "1234567890123456789", "2": "1234567890123456789", "3": "1234567890123456789", "4": "1234567890123456789", "5": "1234567890123456789", "6": "1234567890123456789", }, }, params: { path: { guildId: guildId.toString(), }, }, headers: { Authorization: `Basic ${encoded}`, Origin: "http://localhost:3000", }, }); if (putTimePlanningResponse.error) throw new Error(putTimePlanningResponse.error.error); getConfigResponse = await GET("/api/{guildId}/config", { params: { path: { guildId: guildId.toString(), }, }, headers: { Authorization: `Basic ${encoded}`, Origin: "http://localhost:3000", }, }); if (getConfigResponse.error) throw new Error(getConfigResponse.error.error); switch (getConfigResponse.data?.checksum) { case "843ea341487f777b614f4c1a07b19730a7fd12e3": // webkit case "8c4909abb19f7ca520840c54697f78ca4d0b5089": // firefox case "17701fc9adcffc4df764f35774a752d3a9b43017": // chromium break; default: throw new Error( "After guild GET checksum didn't matched known ones: " + getConfigResponse.data?.checksum, ); } }); });