diff --git a/website/cypress.config.js b/website/cypress.config.js index 9610624f..7d391f23 100644 --- a/website/cypress.config.js +++ b/website/cypress.config.js @@ -22,4 +22,11 @@ export default defineConfig({ getCompareSnapshotsPlugin(on, config); }, }, + + env: { + MAILDEV_PROTOCOL: "http", + MAILDEV_HOST: "localhost", + MAILDEV_SMTP_PORT: "1025", + MAILDEV_API_PORT: "1080", + }, }); diff --git a/website/cypress/e2e/auth/signin.cy.ts b/website/cypress/e2e/auth/signin.cy.ts index b6914016..e87d4b88 100644 --- a/website/cypress/e2e/auth/signin.cy.ts +++ b/website/cypress/e2e/auth/signin.cy.ts @@ -1,10 +1,41 @@ +import { faker } from "@faker-js/faker"; + describe("signin flow", () => { it("redirects to a confirmation page on submit of valid email address", () => { cy.visit("/auth/signin"); - cy.get(".chakra-input").type(`test@example.com`); - cy.get(".chakra-stack > .chakra-button").click(); + cy.get('form[data-cy="signin-email"').within(() => { + cy.get(".chakra-input").type(`test@example.com`); + cy.get(".chakra-stack > .chakra-button").click(); + }); cy.url().should("contain", "/auth/verify"); }); + it("emails a login link to the user when signing in with email", () => { + // Use random email to avoid possibility of tests passing just due to other tests or previous runs also causing emails to be sent + const emailAddress = faker.internet.email(); + cy.log("emailAddress", emailAddress); + cy.request("GET", "/api/auth/csrf") + .then((response) => { + const csrfToken = response.body.csrfToken; + cy.request("POST", "/api/auth/signin/email", { + callbackUrl: "/", + email: emailAddress, + csrfToken, + json: "true", + }); + }) + .then((response) => { + cy.signInUsingEmailedLink(emailAddress).then(() => { + cy.get('[data-cy="username"]').should("exist"); + }); + }); + }); + it("shows the logged in users email address if logged in with email", () => { + const emailAddress = "user@example.com"; + cy.signInWithEmail(emailAddress); + // The user will only see the email address if the window is wide enough, not technically required as even when hidden this will find it in the page. + cy.viewport(1920, 1000); + cy.contains('[data-cy="username"]', emailAddress); + }); }); export {}; diff --git a/website/cypress/support/commands.ts b/website/cypress/support/commands.ts index 2ed74fb3..096720d0 100644 --- a/website/cypress/support/commands.ts +++ b/website/cypress/support/commands.ts @@ -36,4 +36,38 @@ // } // } +Cypress.Commands.add("signInUsingEmailedLink", (emailAddress) => { + const mailDevApi = `${Cypress.env("MAILDEV_PROTOCOL")}://${Cypress.env( + "MAILDEV_HOST" + )}:${Cypress.env("MAILDEV_API_PORT")}`; + cy.request( + "GET", + `${mailDevApi}/email?headers.to=${emailAddress.toLowerCase()}` + ).then((response) => { + const emails = response.body; + + // Find and use login link + const loginLink = emails + .pop() + .html.match(/href="[^"]+(\/api\/auth\/callback\/[^"]+?)"/)[1]; + cy.visit(loginLink); + }); +}); + +Cypress.Commands.add("signInWithEmail", (emailAddress) => { + cy.request("GET", "/api/auth/csrf") + .then((response) => { + const csrfToken = response.body.csrfToken; + cy.request("POST", "/api/auth/signin/email", { + callbackUrl: "/", + email: emailAddress, + csrfToken, + json: "true", + }); + }) + .then(() => { + cy.signInUsingEmailedLink(emailAddress); + }); +}); + export {}; diff --git a/website/cypress/support/e2e.ts b/website/cypress/support/e2e.ts index 4c9b7b84..1ed26be7 100644 --- a/website/cypress/support/e2e.ts +++ b/website/cypress/support/e2e.ts @@ -13,12 +13,8 @@ // https://on.cypress.io/configuration // *********************************************************** -// Import commands.js using ES2015 syntax: import "./commands"; import compareSnapshotCommand from "cypress-image-diff-js/dist/command"; compareSnapshotCommand(); -// Alternatively you can use CommonJS syntax: -// require('./commands') - export {}; diff --git a/website/cypress/support/index.ts b/website/cypress/support/index.ts new file mode 100644 index 00000000..80fba7eb --- /dev/null +++ b/website/cypress/support/index.ts @@ -0,0 +1,22 @@ +// load type definitions that come with Cypress module +/// + +declare global { + namespace Cypress { + interface Chainable { + /** + * Custom command to sign in with a given email address + * @example cy.signInWithEmail('user@example.com') + */ + signInWithEmail(emailAddress: string): Chainable; + + /** + * Custom command to sign in with the link emailed to the given email address + * @example cy.signInUsingEmailedLink('user@example.com') + */ + signInUsingEmailedLink(emailAddress: string): Chainable; + } + } +} + +export {}; diff --git a/website/package-lock.json b/website/package-lock.json index 1587c01e..38f7446e 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -44,6 +44,7 @@ "devDependencies": { "@babel/core": "^7.20.7", "@chakra-ui/storybook-addon": "^4.0.16", + "@faker-js/faker": "^7.6.0", "@storybook/addon-actions": "^6.5.15", "@storybook/addon-essentials": "^6.5.15", "@storybook/addon-interactions": "^6.5.15", @@ -3591,6 +3592,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -31099,6 +31110,12 @@ } } }, + "@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/website/package.json b/website/package.json index bc1f3b69..2bd531a0 100644 --- a/website/package.json +++ b/website/package.json @@ -54,6 +54,7 @@ "devDependencies": { "@babel/core": "^7.20.7", "@chakra-ui/storybook-addon": "^4.0.16", + "@faker-js/faker": "^7.6.0", "@storybook/addon-actions": "^6.5.15", "@storybook/addon-essentials": "^6.5.15", "@storybook/addon-interactions": "^6.5.15", diff --git a/website/src/components/Header/UserMenu.tsx b/website/src/components/Header/UserMenu.tsx index c42d8895..cbd71787 100644 --- a/website/src/components/Header/UserMenu.tsx +++ b/website/src/components/Header/UserMenu.tsx @@ -34,7 +34,9 @@ export function UserMenu() { height="40" className="rounded-full" > -

{session.user.name || session.user.email}

+

+ {session.user.name || session.user.email} +

diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index 2ead2414..a1af2326 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -41,7 +41,7 @@ export default function Signin({ csrfToken, providers }) { )} {email && ( -
+