diff --git a/frontend/blog_portal/cypress/support/commands.ts b/frontend/blog_portal/cypress/support/commands.ts index a25fbf7..526f445 100644 --- a/frontend/blog_portal/cypress/support/commands.ts +++ b/frontend/blog_portal/cypress/support/commands.ts @@ -1,15 +1,104 @@ /// +interface MailpitAddress { + Address: string; + Name: string; +} + +interface MailpitMessageSummary { + ID: string; + From: MailpitAddress; + To: MailpitAddress[]; + Subject: string; + Snippet: string; + Created: string; +} + +interface MailpitMessages { + total: number; + unread: number; + count: number; + messages: MailpitMessageSummary[]; +} + +interface MailpitMessageBody { + ID: string; + Text: string; + HTML: string; +} + +interface SignupArgs { + email: string; + displayName: string; +} + +interface ConfirmArgs { + token: string; + password: string; +} + +interface LoginArgs { + email: string; + password: string; +} + +interface SeedConfirmedUserArgs { + email: string; + displayName: string; + password: string; +} + +type SeedAdminArgs = SeedConfirmedUserArgs; + +interface SeedPostArgs { + email: string; + password: string; + title: string; + body: string; +} + +interface CreatedPost { + id: number; + userId: number; + authorDisplayName: string; + title: string; + body: string; + createdAt: string; + featureSlot: number | null; +} + +interface SeedFeaturedPostArgs { + adminEmail: string; + adminPassword: string; + postId: number; + slot: number; +} + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { resetDb(): Chainable; seedDb(): Chainable; + promoteAdmin(email: string): Chainable; + clearMail(): Chainable; + getMail(): Chainable; + getMailBody(id: string): Chainable; + signupViaApi(args: SignupArgs): Chainable; + fetchLatestConfirmToken(email: string): Chainable; + confirmViaApi(args: ConfirmArgs): Chainable; + loginViaApi(args: LoginArgs): Chainable; + logoutViaApi(): Chainable; + seedConfirmedUser(args: SeedConfirmedUserArgs): Chainable; + seedAdmin(args: SeedAdminArgs): Chainable; + seedPostAs(args: SeedPostArgs): Chainable; + seedFeaturedPost(args: SeedFeaturedPostArgs): Chainable; } } } +const apiBase = "/api"; + Cypress.Commands.add("resetDb", function () { return cy.task("db:reset"); }); @@ -18,4 +107,129 @@ Cypress.Commands.add("seedDb", function () { return cy.task("db:seed"); }); +Cypress.Commands.add("promoteAdmin", function (email: string) { + return cy.task("db:promote", email); +}); + +Cypress.Commands.add("clearMail", function () { + return cy.task("mailpit:clear"); +}); + +Cypress.Commands.add("getMail", function () { + return cy.task("mailpit:messages"); +}); + +Cypress.Commands.add("getMailBody", function (id: string) { + return cy.task("mailpit:message", id); +}); + +Cypress.Commands.add("signupViaApi", function (args: SignupArgs) { + cy.request({ + method: "POST", + url: `${apiBase}/signup`, + body: { email: args.email, displayName: args.displayName }, + }).then(function (response) { + expect(response.status).to.equal(201); + }); +}); + +Cypress.Commands.add("fetchLatestConfirmToken", function (email: string) { + return cy.getMail().then(function (inbox) { + const match = inbox.messages.find(function (message) { + return message.To.some(function (to) { + return to.Address.toLowerCase() === email.toLowerCase(); + }); + }); + if (!match) { + throw new Error(`no mailpit message for ${email}`); + } + return cy.getMailBody(match.ID).then(function (body) { + const text = body.Text || body.HTML; + const tokenMatch = text.match(/token=([a-f0-9]+)/); + if (!tokenMatch) { + throw new Error("confirmation token not found in mail body"); + } + return tokenMatch[1]; + }); + }); +}); + +Cypress.Commands.add("confirmViaApi", function (args: ConfirmArgs) { + cy.request({ + method: "POST", + url: `${apiBase}/confirm-email`, + body: { token: args.token, password: args.password }, + }).then(function (response) { + expect(response.status).to.equal(200); + }); +}); + +Cypress.Commands.add("loginViaApi", function (args: LoginArgs) { + cy.request({ + method: "POST", + url: `${apiBase}/login`, + body: { email: args.email, password: args.password }, + }).then(function (response) { + expect(response.status).to.equal(200); + }); +}); + +Cypress.Commands.add("logoutViaApi", function () { + cy.request({ + method: "POST", + url: `${apiBase}/logout`, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add( + "seedConfirmedUser", + function (args: SeedConfirmedUserArgs) { + cy.signupViaApi({ email: args.email, displayName: args.displayName }); + cy.fetchLatestConfirmToken(args.email).then(function (token) { + cy.confirmViaApi({ token, password: args.password }); + }); + cy.logoutViaApi(); + }, +); + +Cypress.Commands.add("seedAdmin", function (args: SeedAdminArgs) { + cy.seedConfirmedUser({ + email: args.email, + displayName: args.displayName, + password: args.password, + }); + cy.promoteAdmin(args.email); +}); + +Cypress.Commands.add("seedPostAs", function (args: SeedPostArgs) { + cy.loginViaApi({ email: args.email, password: args.password }); + return cy + .request({ + method: "POST", + url: `${apiBase}/posts`, + body: { title: args.title, body: args.body }, + }) + .then(function (response) { + expect(response.status).to.equal(201); + cy.logoutViaApi(); + return response.body.post as CreatedPost; + }); +}); + +Cypress.Commands.add( + "seedFeaturedPost", + function (args: SeedFeaturedPostArgs) { + cy.loginViaApi({ email: args.adminEmail, password: args.adminPassword }); + cy.request({ + method: "POST", + url: `${apiBase}/admin/posts/feature`, + body: { postId: args.postId, slot: args.slot }, + }).then(function (response) { + expect(response.status).to.equal(200); + }); + cy.logoutViaApi(); + }, +); + export {};