diff --git a/cypress.config.ts b/cypress.config.ts index bc2b3262..688c833f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -13,11 +13,16 @@ export default defineConfig({ }, // Need for waiting api server defaultCommandTimeout: 60000, + requestTimeout: 120000, viewportWidth: 1200, baseUrl: "http://localhost:3000", watchForFileChanges: false, }, env: { apiUrl: process.env.NEXT_PUBLIC_API_URL, + admin: { + email: process.env.TEST_ADMIN_USER, + password: process.env.TEST_ADMIN_PASSWORD, + }, }, }); diff --git a/cypress/e2e/1-auth/admin-panel.cy.ts b/cypress/e2e/1-auth/admin-panel.cy.ts new file mode 100644 index 00000000..f750acf6 --- /dev/null +++ b/cypress/e2e/1-auth/admin-panel.cy.ts @@ -0,0 +1,362 @@ +/// + +import { customAlphabet } from "nanoid"; +const nanoid = customAlphabet("0123456789qwertyuiopasdfghjklzxcvbnm", 10); +const adminUser = Cypress.env("admin"); + +let email: string; +let password: string; +let firstName: string; +let lastName: string; +let userId: string; + +describe("New user creation", () => { + beforeEach(() => { + email = `test${nanoid()}@example.com`; + password = nanoid(); + firstName = `FirstName${nanoid()}`; + lastName = `LastName${nanoid()}`; + + cy.intercept("GET", "api/v1/users?*").as("usersList"); + cy.intercept("POST", "/api/v1/users").as("userCreated"); + + cy.visit("/sign-up"); + cy.login({ + email: adminUser.email, + password: adminUser.password, + }); + }); + + it("should create a new user", () => { + cy.getBySel("users-list").should("be.visible"); + cy.wait(3000); + cy.getBySel("users-list").click(); + cy.wait("@usersList").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.get("table").should("be.visible"); + cy.log("Users list is loaded and displayed"); + + cy.getBySel("create-user").click(); + cy.location("pathname").should("include", "/admin-panel/users/create"); + cy.getBySel("new-user-email").type(email); + cy.getBySel("new-user-password").type(password); + cy.getBySel("new-user-password-confirmation").type(password); + cy.getBySel("first-name").type(firstName); + cy.getBySel("last-name").type(lastName); + cy.getBySel("role").children("div").should("contain.text", "User"); + cy.getBySel("save-user").click(); + cy.wait("@userCreated").then((request) => { + expect(request.response?.statusCode).to.equal(201); + }); + cy.log("New user is created"); + cy.logout(); + cy.login({ email, password }); + cy.getBySel("users-list").should("not.exist"); + cy.log("New user can login in the system"); + }); + + it("should create a new admin user", () => { + cy.visit("/admin-panel/users/create"); + cy.getBySel("new-user-email").type(email); + cy.getBySel("new-user-password").type(password); + cy.getBySel("new-user-password-confirmation").type(password); + cy.getBySel("first-name").type(firstName); + cy.getBySel("last-name").type(lastName); + cy.getBySel("role").children("div").should("contain.text", "User"); + cy.getBySel("role").click(); + cy.getBySel("1").click(); + cy.getBySel("role").children("div").should("contain.text", "Admin"); + cy.getBySel("save-user").click(); + cy.wait("@userCreated").then((request) => { + expect(request.response?.statusCode).to.equal(201); + }); + cy.log("New user is created"); + cy.logout(); + cy.login({ email, password }); + cy.getBySel("users-list").should("be.visible"); + cy.log("New user can login in the system"); + }); + + it("should display validation errors for required fields", () => { + cy.visit("/admin-panel/users/create"); + cy.getBySel("save-user").click(); + cy.getBySel("first-name-error").should("be.visible"); + cy.getBySel("last-name-error").should("be.visible"); + cy.getBySel("new-user-email-error").should("be.visible"); + cy.getBySel("new-user-password-error").should("be.visible"); + cy.getBySel("new-user-password-confirmation-error").should("be.visible"); + cy.log("Error for required is displayed"); + + cy.getBySel("new-user-email").type(email); + cy.getBySel("new-user-email-error").should("not.exist"); + cy.getBySel("new-user-password").type(password); + cy.getBySel("new-user-password-error").should("not.exist"); + cy.getBySel("new-user-password-confirmation").type(password); + cy.getBySel("new-user-password-confirmation-error").should("not.exist"); + cy.getBySel("first-name").type(firstName); + cy.getBySel("first-name-error").should("not.exist"); + cy.getBySel("last-name").type(lastName); + cy.getBySel("last-name-error").should("not.exist"); + cy.getBySel("save-user").click(); + cy.wait("@userCreated").then((request) => { + expect(request.response?.statusCode).to.equal(201); + }); + cy.log("New user is created"); + }); + + it("should validate users password", () => { + const newPassword = "passw1"; + + cy.visit("/admin-panel/users/create"); + cy.getBySel("new-user-email").type(email); + cy.getBySel("first-name").type(firstName); + cy.getBySel("last-name").type(lastName); + cy.getBySel("role").children("div").should("contain.text", "User"); + + cy.getBySel("new-user-password").type("passw{enter}"); + cy.getBySel("new-user-password-error").should("be.visible"); + cy.getBySel("new-user-password").type("1{enter}"); + cy.getBySel("new-user-password-error").should("not.exist"); + + cy.getBySel("new-user-password-confirmation").type(newPassword); + cy.getBySel("new-user-password-confirmation-error").should("not.exist"); + cy.getBySel("new-user-password-confirmation").type( + "{selectAll}different password" + ); + cy.getBySel("new-user-password-confirmation-error").should("be.visible"); + cy.getBySel("new-user-password-confirmation").type( + `{selectAll}${newPassword}` + ); + cy.getBySel("new-user-password-confirmation-error").should("not.exist"); + + cy.getBySel("save-user").click(); + cy.wait("@userCreated").then((request) => { + expect(request.response?.statusCode).to.equal(201); + }); + cy.log("New user is created"); + }); + + it('should display "want to leave" modal', () => { + cy.visit("/admin-panel/users/create"); + cy.getBySel("new-user-email").type(email); + cy.getBySel("cancel-user").click(); + cy.getBySel("want-to-leave-modal").should("be.visible"); + cy.getBySel("stay").click(); + cy.getBySel("want-to-leave-modal").should("not.exist"); + cy.location("pathname").should("include", "/admin-panel/users/create"); + + cy.getBySel("home-page").click(); + cy.getBySel("want-to-leave-modal").should("be.visible"); + cy.getBySel("stay").click(); + cy.getBySel("want-to-leave-modal").should("not.exist"); + cy.location("pathname").should("include", "/admin-panel/users/create"); + + cy.getBySel("cancel-user").click(); + cy.getBySel("want-to-leave-modal").should("be.visible"); + cy.getBySel("leave").click(); + cy.location("pathname").should("include", "/admin-panel/users"); + }); +}); + +describe("Edit users", () => { + beforeEach(() => { + email = `test${nanoid()}@example.com`; + password = nanoid(); + firstName = `FirstName${nanoid()}`; + lastName = `LastName${nanoid()}`; + + cy.intercept("GET", "api/v1/users?*").as("usersList"); + cy.intercept("GET", "api/v1/users/*").as("userProfile"); + cy.intercept("PATCH", "/api/v1/users/*").as("profileUpdate"); + cy.intercept("POST", "/api/v1/auth/email/login").as("login1"); + + cy.createNewUser({ + email, + password, + firstName, + lastName, + }); + cy.visit("/sign-in"); + cy.getBySel("email").type(email); + cy.getBySel("password").type(password); + cy.getBySel("sign-in-submit").click(); + cy.wait("@login1").then((request) => { + userId = request.response?.body.user.id; + }); + cy.logout(); + + cy.login({ + email: adminUser.email, + password: adminUser.password, + }); + }); + + it("should edit a user", () => { + cy.visit(`/admin-panel/users/edit/${userId}`); + cy.wait("@userProfile").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.wait(3000); + cy.get('[data-testid="first-name"] input').should( + "contain.value", + firstName + ); + cy.getBySel("first-name").type(`{selectAll}James`); + cy.get('[data-testid="first-name"] input').should("contain.value", "James"); + cy.get('[data-testid="last-name"] input').should("contain.value", lastName); + cy.getBySel("last-name").type(`{selectAll}Bond`); + cy.get('[data-testid="last-name"] input').should("contain.value", "Bond"); + cy.getBySel("save-profile").click(); + + cy.wait("@profileUpdate").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.getBySel("users-list").should("be.visible"); + cy.wait(3000); + cy.getBySel("users-list").click(); + cy.wait("@usersList").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.get("table").should("be.visible"); + cy.contains(email) + .parent() + .within(() => { + cy.contains("Edit").click({ force: true }); + }); + }); + + it("should change user role from user to admin", () => { + cy.visit(`/admin-panel/users/edit/${userId}`); + cy.wait("@userProfile").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.wait(3000); + cy.getBySel("role").children("div").should("contain.text", "User"); + cy.getBySel("role").click(); + cy.getBySel("1").click({ force: true }); + cy.getBySel("role").children("div").should("contain.text", "Admin"); + cy.getBySel("save-profile").click(); + cy.wait("@profileUpdate").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + + cy.logout(); + cy.login({ email, password }); + cy.getBySel("users-list").should("be.visible"); + cy.wait(3000); + cy.getBySel("users-list").click(); + cy.wait("@usersList").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.get("table").should("be.visible"); + cy.log("User is logged in and can access Users table"); + }); + + it("should change users password", () => { + const newPassword = "passw1"; + cy.visit(`/admin-panel/users/edit/${userId}`); + cy.wait("@userProfile").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.getBySel("password").type(newPassword); + cy.getBySel("password-confirmation").type(newPassword); + cy.getBySel("save-password").click(); + cy.wait("@profileUpdate").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.log("User password is changed"); + + cy.logout(); + cy.getBySel("sign-in").click(); + cy.location("pathname").should("include", "/sign-in"); + cy.getBySel("email").type(email); + cy.getBySel("password").type(password); + cy.getBySel("sign-in-submit").click(); + cy.wait("@login").then((request) => { + expect(request.response?.statusCode).to.equal(422); + }); + cy.getBySel("password-error").should("be.visible"); + cy.log("User cannot login with old password"); + + cy.getBySel("password").type(`{selectAll}${newPassword}`); + cy.getBySel("sign-in-submit").click(); + cy.wait("@login").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.location("pathname").should("not.include", "/sign-in"); + }); +}); + +describe("Edit user role", () => { + beforeEach(() => { + email = `test${nanoid()}@example.com`; + password = nanoid(); + firstName = `FirstName${nanoid()}`; + lastName = `LastName${nanoid()}`; + + cy.intercept("GET", "api/v1/users?*").as("usersList"); + cy.intercept("POST", "/api/v1/users").as("userCreated"); + cy.intercept("GET", "api/v1/users/*").as("userProfile"); + cy.intercept("PATCH", "/api/v1/users/*").as("profileUpdate"); + + cy.visit("/sign-up"); + cy.login({ + email: adminUser.email, + password: adminUser.password, + }); + cy.visit("/admin-panel/users/create"); + cy.getBySel("new-user-email").type(email); + cy.getBySel("new-user-password").type(password); + cy.getBySel("new-user-password-confirmation").type(password); + cy.getBySel("first-name").type(firstName); + cy.getBySel("last-name").type(lastName); + cy.getBySel("role").children("div").should("contain.text", "User"); + cy.getBySel("role").click(); + cy.getBySel("1").click({ force: true }); + cy.getBySel("role").children("div").should("contain.text", "Admin"); + cy.getBySel("save-user").click(); + cy.wait("@userCreated").then((request) => { + expect(request.response?.statusCode).to.equal(201); + userId = request.response?.body.id; + cy.log(userId); + }); + cy.log("New admin user is created"); + + cy.logout(); + cy.login({ email, password }); + cy.getBySel("users-list").should("be.visible"); + cy.wait(3000); + cy.getBySel("users-list").click(); + cy.wait("@usersList").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.get("table").should("be.visible"); + cy.log("User is logged in and have access to users table"); + + cy.logout(); + }); + + it("should change user role from admin to user", () => { + cy.login({ email: adminUser.email, password: adminUser.password }); + cy.visit(`/admin-panel/users/edit/${userId}`); + cy.wait("@userProfile").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.wait(3000); + cy.getBySel("role").children("div").should("contain.text", "Admin"); + cy.getBySel("role").click(); + cy.getBySel("2").click(); + cy.getBySel("role").children("div").should("contain.text", "User"); + cy.getBySel("save-profile").click(); + cy.wait("@profileUpdate").then((request) => { + expect(request.response?.statusCode).to.equal(200); + }); + cy.log("Users role changed from admin to user"); + + cy.logout(); + cy.login({ email, password }); + cy.getBySel("users-list").should("not.exist"); + cy.log("User is logged in and doesn't have access to users table"); + }); +}); diff --git a/cypress/e2e/1-auth/sign-in.cy.ts b/cypress/e2e/1-auth/sign-in.cy.ts index f9ebb6df..1e271392 100644 --- a/cypress/e2e/1-auth/sign-in.cy.ts +++ b/cypress/e2e/1-auth/sign-in.cy.ts @@ -2,6 +2,7 @@ import { customAlphabet } from "nanoid"; const nanoid = customAlphabet("0123456789qwertyuiopasdfghjklzxcvbnm", 10); +const adminUser = Cypress.env("admin"); describe("Sign In", () => { context("main flow", () => { @@ -28,6 +29,14 @@ describe("Sign In", () => { cy.location("pathname").should("not.include", "/sign-in"); }); + it("should be successful for admin user", () => { + cy.getBySel("email").type(adminUser.email); + cy.getBySel("password").type(adminUser.password); + cy.getBySel("sign-in-submit").click(); + cy.location("pathname").should("not.include", "/sign-in"); + cy.getBySel("users-list").should("be.visible"); + }); + it("should be successful with redirect", () => { cy.visit("/profile"); cy.location("pathname").should("include", "/sign-in"); diff --git a/cypress/e2e/1-auth/user-profile.cy.ts b/cypress/e2e/1-auth/user-profile.cy.ts index becc8a4c..c07692fd 100644 --- a/cypress/e2e/1-auth/user-profile.cy.ts +++ b/cypress/e2e/1-auth/user-profile.cy.ts @@ -97,10 +97,14 @@ describe("User Profile", () => { cy.getBySel("first-name").type(`{selectAll}James`); cy.getBySel("cancel-edit-profile").click(); - cy.getBySel("want-to-leave-modal").should("be.visible"); cy.getBySel("stay").click(); + cy.getBySel("want-to-leave-modal").should("not.exist"); + cy.location("pathname").should("include", "/profile/edit"); + cy.getBySel("home-page").click(); + cy.getBySel("want-to-leave-modal").should("be.visible"); + cy.getBySel("stay").click(); cy.getBySel("want-to-leave-modal").should("not.exist"); cy.location("pathname").should("include", "/profile/edit"); cy.get('[data-testid="first-name"] input').should("contain.value", "James"); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index bc61877d..38db3db6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -43,6 +43,20 @@ Cypress.Commands.add("login", ({ email, password }) => { cy.getBySel("sign-in-submit").click(); cy.wait("@login"); cy.location("pathname").should("not.include", "/sign-in"); + cy.getCookie("auth-token-data").should("exist"); +}); + +Cypress.Commands.add("logout", () => { + cy.intercept("POST", "/api/v1/auth/logout").as("logout"); + cy.getBySel("profile-menu-item").click(); + cy.getBySel("logout-menu-item").click(); + cy.wait("@logout").then((request) => { + expect(request.response?.statusCode).to.equal(204); + }); + cy.getBySel("home-page").should("be.visible"); + cy.getBySel("sign-in").should("be.visible"); + cy.getBySel("sign-up").should("be.visible"); + cy.getCookie("auth-token-data").should("not.exist"); }); export {}; @@ -58,6 +72,7 @@ declare global { }): Chainable; getBySel(selector: string): Chainable>; login(params: { email: string; password: string }): Chainable; + logout(): Chainable; } } } diff --git a/example.env.local b/example.env.local index 9d4352df..cf88102b 100644 --- a/example.env.local +++ b/example.env.local @@ -15,3 +15,6 @@ TEST_IMAP_PASSWORD=SMSTVykf66Pw3NYPg6 TEST_IMAP_HOST=imap.ethereal.email TEST_IMAP_PORT=993 TEST_IMAP_TLS=true + +TEST_ADMIN_USER=admin@example.com +TEST_ADMIN_PASSWORD=secret \ No newline at end of file diff --git a/src/app/[language]/admin-panel/users/create/page-content.tsx b/src/app/[language]/admin-panel/users/create/page-content.tsx index 87ee52a4..b4abd647 100644 --- a/src/app/[language]/admin-panel/users/create/page-content.tsx +++ b/src/app/[language]/admin-panel/users/create/page-content.tsx @@ -39,9 +39,7 @@ const useValidationSchema = () => { email: yup .string() .email(t("admin-panel-users-create:inputs.email.validation.invalid")) - .required( - t("admin-panel-users-create:inputs.firstName.validation.required") - ), + .required(t("admin-panel-users-create:inputs.email.validation.required")), firstName: yup .string() .required( @@ -92,6 +90,7 @@ function CreateUserFormActions() { color="primary" type="submit" disabled={isSubmitting} + data-testid="save-user" > {t("admin-panel-users-create:actions.submit")} @@ -234,6 +233,7 @@ function FormCreateUser() { color="inherit" LinkComponent={Link} href="/admin-panel/users" + data-testid="cancel-user" > {t("admin-panel-users-create:actions.cancel")} diff --git a/src/app/[language]/admin-panel/users/edit/[id]/page-content.tsx b/src/app/[language]/admin-panel/users/edit/[id]/page-content.tsx index 3c1ea40a..728805b0 100644 --- a/src/app/[language]/admin-panel/users/edit/[id]/page-content.tsx +++ b/src/app/[language]/admin-panel/users/edit/[id]/page-content.tsx @@ -103,6 +103,7 @@ function EditUserFormActions() { variant="contained" color="primary" type="submit" + data-testid="save-profile" disabled={isSubmitting} > {t("admin-panel-users-edit:actions.submit")} @@ -120,6 +121,7 @@ function ChangePasswordUserFormActions() { variant="contained" color="primary" type="submit" + data-testid="save-password" disabled={isSubmitting} > {t("admin-panel-users-edit:actions.submit")} @@ -336,6 +338,7 @@ function FormChangePasswordUser() { name="password" type="password" + testId="password" label={t("admin-panel-users-edit:inputs.password.label")} /> @@ -343,6 +346,7 @@ function FormChangePasswordUser() { name="passwordConfirmation" + testId="password-confirmation" label={t( "admin-panel-users-edit:inputs.passwordConfirmation.label" )} diff --git a/src/app/[language]/admin-panel/users/page-content.tsx b/src/app/[language]/admin-panel/users/page-content.tsx index 54da08fe..fead590e 100644 --- a/src/app/[language]/admin-panel/users/page-content.tsx +++ b/src/app/[language]/admin-panel/users/page-content.tsx @@ -159,6 +159,7 @@ function Actions({ user }: { user: User }) { size="small" variant="contained" LinkComponent={Link} + data-testid="edit" href={`/admin-panel/users/edit/${user.id}`} > {tUsers("admin-panel-users:actions.edit")} @@ -219,6 +220,7 @@ function Actions({ user }: { user: User }) { bgcolor: "error.light", }, }} + data-testid="delete" onClick={handleDelete} > {tUsers("admin-panel-users:actions.delete")} @@ -312,6 +314,7 @@ function Users() { LinkComponent={Link} href="/admin-panel/users/create" color="success" + data-testid="create-user" > {tUsers("admin-panel-users:actions.create")} diff --git a/src/components/app-bar.tsx b/src/components/app-bar.tsx index 5cb22a4d..fdef757c 100644 --- a/src/components/app-bar.tsx +++ b/src/components/app-bar.tsx @@ -166,6 +166,7 @@ function ResponsiveAppBar() { sx={{ my: 2, color: "white", display: "block" }} component={Link} href="/" + data-testid="home-page" > {t("common:navigation.home")} @@ -176,6 +177,7 @@ function ResponsiveAppBar() { sx={{ my: 2, color: "white", display: "block" }} component={Link} href="/admin-panel/users" + data-testid="users-list" > {t("common:navigation.users")} @@ -245,6 +247,7 @@ function ResponsiveAppBar() { sx={{ my: 2, color: "white", display: "block" }} component={Link} href="/sign-in" + data-testid="sign-in" > {t("common:navigation.signIn")} @@ -253,6 +256,7 @@ function ResponsiveAppBar() { sx={{ my: 2, color: "white", display: "block" }} component={Link} href="/sign-up" + data-testid="sign-up" > {t("common:navigation.signUp")} diff --git a/src/components/confirm-dialog/confirm-dialog-provider.tsx b/src/components/confirm-dialog/confirm-dialog-provider.tsx index 0b99accf..40042d70 100644 --- a/src/components/confirm-dialog/confirm-dialog-provider.tsx +++ b/src/components/confirm-dialog/confirm-dialog-provider.tsx @@ -76,6 +76,7 @@ function ConfirmDialogProvider({ children }: { children: React.ReactNode }) { onClose={handleClose} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" + data-testid="confirm-dialog" > {confirmDialogInfo.title} @@ -86,10 +87,10 @@ function ConfirmDialogProvider({ children }: { children: React.ReactNode }) { - - diff --git a/src/components/form/select/form-select.tsx b/src/components/form/select/form-select.tsx index 2907dc49..84b8191d 100644 --- a/src/components/form/select/form-select.tsx +++ b/src/components/form/select/form-select.tsx @@ -65,6 +65,7 @@ function SelectInputRaw( {props.options.map((option) => ( {props.renderOption(option)}