diff --git a/.env.example b/.env.example index 4a88fb2..8f15a8e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +# APP_ENV=test # if you wish to use test mode (no real github oauth) + NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID= FLAGSMITH_ENVIRONMENT_API_KEY= diff --git a/README.md b/README.md index a03e48b..7335bc0 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ How friendly is your GitHub Open Source Repo? This project will check to make su b. In your GitHub OAuth app, GitHub will generate the `client id` and `client secret`, add these to your the `.env` file + You can change the environment variable `APP_ENV` to the value of `test`, and this will mock out the OAuth. + 9. Run the project with one of these a. If you have Postgres installed, you can run the app locally `npm run dev` OR diff --git a/package.json b/package.json index fa3e6de..7a81a7e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "set TRUNK_TELEMETRY=off && trunk check", "fmt": "set TRUNK_TELEMETRY=off && trunk fmt", "lint-old": "next lint", - "test": "npx playwright test", + "test": "npx playwright test --trace on", "prepare": "husky", "postinstall": "shx cp -n ./.env.example ./.env", "format:write": "prettier . --write", diff --git a/playwright.config.ts b/playwright.config.ts index af5293f..c1a5472 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,7 +25,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://127.0.0.1:3000", + baseURL: "http://localhost:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -71,8 +71,9 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "cross-env APP_ENV=test npm run dev", - url: "http://127.0.0.1:3000", + command: + "cross-env APP_ENV=test GITHUB_ID=aaa GITHUB_SECRET=bbb npm run dev", + url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, }); diff --git a/src/app/api/auth/[...nextauth]/route.js b/src/app/api/auth/[...nextauth]/route.js index 460907a..9eb5fa3 100644 --- a/src/app/api/auth/[...nextauth]/route.js +++ b/src/app/api/auth/[...nextauth]/route.js @@ -5,19 +5,24 @@ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); +let githubProvierConfig = { + clientId: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET, + authorization: { + params: { + scope: "read:user user:email public_repo read:project", + }, + }, +}; + +if (process.env.NEXT_RUNTIME === "nodejs" && process.env.APP_ENV === "test") { + githubProvierConfig.authorization.url = + "http://localhost:3000/api/auth/callback/github?code=abcd"; +} + const authOptions = { adapter: PrismaAdapter(prisma), - providers: [ - GithubProvider({ - clientId: process.env.GITHUB_ID, - clientSecret: process.env.GITHUB_SECRET, - authorization: { - params: { - scope: "read:user user:email public_repo read:project", - }, - }, - }), - ], + providers: [GithubProvider(githubProvierConfig)], callbacks: { async session({ session, token, user }) { session.user.id = user.id; diff --git a/src/instrumentation.js b/src/instrumentation.js index 7a3528e..e91fc7e 100644 --- a/src/instrumentation.js +++ b/src/instrumentation.js @@ -1,6 +1,6 @@ export async function register() { const unmocked = [ - "localhost:3000", + "nextjs.org", "googleapis.com", "gstatic.com", "github.com/mona.png", diff --git a/tests/account/repo/add.spec.ts b/tests/account/repo/add.spec.ts index 4da305b..87a15b2 100644 --- a/tests/account/repo/add.spec.ts +++ b/tests/account/repo/add.spec.ts @@ -2,35 +2,36 @@ import { test, expect } from "@playwright/test"; import { login, logout } from "../../setup/auth"; -test("Guest user cannot access add repo", async ({ browser }) => { - const page = await logout(browser); +test("Guest user cannot access add repo", async ({ page }) => { + await logout(page); await page.goto("/account/repo/add"); - await expect(page).toHaveURL(/\//); + await expect(page).toHaveURL("/"); }); -test("Logged in user can access add repo", async ({ browser }) => { - const page = await login(browser); +test("Logged in user can access add repo", async ({ page }) => { + await login(page); + await page.goto("/account/repo/add"); await expect(page).toHaveURL(/account\/repo\/add/); }); -test("Logged in user can see add user nav button", async ({ browser }) => { - const page = await login(browser); +test("Logged in user can see add user nav button", async ({ page }) => { + await login(page); await page.goto("/"); await page.getByRole("link", { name: "Add" }).click(); await expect(page).toHaveURL(/account\/repo\/add/); }); -test("test url required", async ({ browser }) => { - const page = await login(browser); +test("test url required", async ({ page }) => { + await login(page); await page.goto("/account/repo/add"); await page.getByLabel("url").fill("abcdefg"); await page.getByRole("button", { name: "SAVE" }).click(); await expect(page.locator("#url-error")).toContainText("Invalid url"); }); -test("test valid url navigates to the check list page", async ({ browser }) => { - const page = await login(browser); +test("test valid url navigates to the check list page", async ({ page }) => { + await login(page); await page.goto("/account/repo/add"); await page diff --git a/tests/data/flagsmith/flags.json b/tests/data/flagsmith/flags.json index fe51488..ca61c30 100644 --- a/tests/data/flagsmith/flags.json +++ b/tests/data/flagsmith/flags.json @@ -1 +1,70 @@ -[] +[ + { + "id": 1, + "feature": { + "id": 1, + "name": "tagline", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "How friendly is your Open Source Repo?", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "How friendly is your Open Source Repo?", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 2, + "feature": { + "id": 2, + "name": "news", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "Give our GitHub repo a Star", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "Give our GitHub repo a Star", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 3, + "feature": { + "id": 3, + "name": "banner", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "A big thank you to Flagsmith for sponsoring this Open Source project", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "A big thank you to Flagsmith for sponsoring this Open Source project", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 4, + "feature": { + "id": 5, + "name": "repolimit", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "8", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": 5, + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + } +] diff --git a/tests/data/flagsmith/identities.json b/tests/data/flagsmith/identities.json index fde9a8c..8a1d38c 100644 --- a/tests/data/flagsmith/identities.json +++ b/tests/data/flagsmith/identities.json @@ -1,4 +1,73 @@ { "traits": [], - "flags": [] + "flags": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "tagline", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "How friendly is your Open Source Repo?", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "How friendly is your Open Source Repo?", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 2, + "feature": { + "id": 2, + "name": "news", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "Give our GitHub repo a Star", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "Give our GitHub repo a Star", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 3, + "feature": { + "id": 3, + "name": "banner", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "A big thank you to Flagsmith for sponsoring this Open Source project", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": "A big thank you to Flagsmith for sponsoring this Open Source project", + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + }, + { + "id": 4, + "feature": { + "id": 5, + "name": "repolimit", + "created_date": "2023-05-14T06:11:08.178802Z", + "description": null, + "initial_value": "8", + "default_enabled": true, + "type": "STANDARD" + }, + "feature_state_value": 5, + "environment": 5242, + "identity": null, + "feature_segment": null, + "enabled": true + } + ] } diff --git a/tests/data/github/access_token.json b/tests/data/github/access_token.json new file mode 100644 index 0000000..a77712f --- /dev/null +++ b/tests/data/github/access_token.json @@ -0,0 +1,5 @@ +{ + "access_token": "abcdefg", + "scope": "repo,gist", + "token_type": "bearer" +} diff --git a/tests/data/github/user.json b/tests/data/github/user.json new file mode 100644 index 0000000..f838d30 --- /dev/null +++ b/tests/data/github/user.json @@ -0,0 +1,46 @@ +{ + "login": "mona", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/mona.png", + "gravatar_id": "", + "url": "https://api.github.com/users/mona", + "html_url": "https://github.com/mona", + "followers_url": "https://api.github.com/users/mona/followers", + "following_url": "https://api.github.com/users/mona/following{/other_user}", + "gists_url": "https://api.github.com/users/mona/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mona/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mona/subscriptions", + "organizations_url": "https://api.github.com/users/mona/orgs", + "repos_url": "https://api.github.com/users/mona/repos", + "events_url": "https://api.github.com/users/mona/events{/privacy}", + "received_events_url": "https://api.github.com/users/mona/received_events", + "type": "User", + "site_admin": false, + "name": "monalisa octocat", + "company": "GitHub", + "blog": "https://github.com/blog", + "location": "San Francisco", + "email": "mona@github.com", + "hireable": false, + "bio": "There once was...", + "twitter_username": "monatheoctocat", + "public_repos": 2, + "public_gists": 1, + "followers": 20, + "following": 0, + "created_at": "2008-01-14T04:33:35Z", + "updated_at": "2008-01-14T04:33:35Z", + "private_gists": 81, + "total_private_repos": 100, + "owned_private_repos": 100, + "disk_usage": 10000, + "collaborators": 8, + "two_factor_authentication": true, + "plan": { + "name": "Medium", + "space": 400, + "private_repos": 20, + "collaborators": 0 + } +} diff --git a/tests/setup/auth.js b/tests/setup/auth.js index c07de1b..cd0ff7f 100644 --- a/tests/setup/auth.js +++ b/tests/setup/auth.js @@ -1,120 +1,34 @@ -import { encode } from "next-auth/jwt"; -import prisma from "@/models/db"; - -const login = async ( - browser, - user = { - name: "Authenticated User", - email: "authenticated-user@test.com", - }, -) => { - const date = new Date(); - let testUser; - - const userData = { - email: user.email, - name: user.name, - image: "https://github.com/mona.png", - emailVerified: null, - }; - - try { - testUser = await prisma.user.upsert({ - where: { email: user.email }, - update: userData, - create: userData, - }); - - if (!testUser) { - throw new Error("Failed to create or retrieve test authenticated user"); - } - } catch (e) { - const error = "Test authenticated user creation failed"; - console.error(error, e); - throw new Error(error); - } - - const sessionToken = await encode({ - token: { - image: "https://github.com/mona.png", - accessToken: "ggg_zZl1pWIvKkf3UDynZ09zLvuyZsm1yC0YoRPt", - ...user, - sub: testUser.id, - }, - secret: process.env.NEXTAUTH_SECRET, - }); - - const session = { - sessionToken, - userId: testUser.id, - expires: new Date(date.getFullYear(), date.getMonth() + 1, 0), - }; - - try { - await prisma.session.upsert({ - where: { - sessionToken: sessionToken, - }, - update: session, - create: session, - }); - } catch (e) { - const error = "Test authenticated session creation failed"; - console.error(error, e); - throw new Error(error); - } - - const account = { - type: "oauth", - provider: "github", - providerAccountId: testUser.id, - userId: testUser.id, - access_token: "ggg_zZl1pWIvKkf3UDynZ09zLvuyZsm1yC0YoRPt", - token_type: "bearer", - scope: "read:org,read:user,repo,user:email,test:all", - }; - - try { - await prisma.account.upsert({ - where: { - provider_providerAccountId: { - provider: "github", - providerAccountId: testUser.id, - }, - }, - update: account, - create: account, - }); - } catch (e) { - const error = "Test account creation failed"; - console.error(error, e); - throw new Error(error); +import { expect } from "@playwright/test"; + +const login = async (page) => { + // check if already logged in + const isLoggedIn = await page + .getByRole("button", { name: "Sign out" }) + .isVisible(); + if (isLoggedIn) { + return page; } - const context = await browser.newContext(); - await context.addCookies([ - { - name: "next-auth.session-token", - value: sessionToken, - domain: "127.0.0.1", - path: "/", - httpOnly: true, - sameSite: "Lax", - secure: true, - expires: -1, - }, - ]); + // go to login url + const url = + "http://localhost:3000/api/auth/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F"; + await page.goto(url); + await expect(page).toHaveURL(/\/api\/auth\/signin/); - const page = await context.newPage(); + // click login + await page.getByRole("button", { name: "Sign in with GitHub" }).click(); + await expect(page).toHaveURL("/"); return page; }; -const logout = async (browser) => { - const context = await browser.newContext(); - await context.clearCookies(); +const logout = async (page) => { + // visit sign out page + await page.goto("/api/auth/signout"); - const page = await context.newPage(); + // click sign out + await page.getByRole("button", { name: "Sign out" }).click(); + await expect(page).toHaveURL("/"); return page; }; diff --git a/tests/setup/mocks/handlers.js b/tests/setup/mocks/handlers.js index fcc18d2..dd522ab 100644 --- a/tests/setup/mocks/handlers.js +++ b/tests/setup/mocks/handlers.js @@ -9,10 +9,18 @@ import labels from "../../data/github/labels.json"; import projects from "../../data/github/projects.json"; import referrers from "../../data/github/referrers.json"; import views from "../../data/github/views.json"; +import user from "../../data/github/user.json"; +import accessToken from "../../data/github/access_token.json"; import identities from "../../data/flagsmith/identities.json"; import flags from "../../data/flagsmith/flags.json"; export const handlers = [ + // authentication + http.post("https://github.com/login/oauth/access_token", () => + HttpResponse.json(accessToken), + ), + http.get("https://api.github.com/user", () => HttpResponse.json(user)), + // flagsmith http.post("https://edge.api.flagsmith.com/api/v1/identities/", () => HttpResponse.json(identities),