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) => (