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 {};