diff --git a/.github/workflows/pull-request-test.yml b/.github/workflows/pull-request-test.yml index ddf11abcb..78398052e 100644 --- a/.github/workflows/pull-request-test.yml +++ b/.github/workflows/pull-request-test.yml @@ -150,6 +150,7 @@ jobs: checkWorkflows, rstudioSession, v2/dashboardV2, + v2/projectBasics, ] steps: diff --git a/cypress-tests/cypress/e2e/v2/dashboardV2.cy.ts b/cypress-tests/cypress/e2e/v2/dashboardV2.cy.ts index 2f9667999..e2a90fa1b 100644 --- a/cypress-tests/cypress/e2e/v2/dashboardV2.cy.ts +++ b/cypress-tests/cypress/e2e/v2/dashboardV2.cy.ts @@ -1,11 +1,11 @@ import { getRandomString, validateLogin } from "../../support/commands/general"; import { generatorProjectName } from "../../support/commands/projects"; +import { ProjectIdentifierV2 } from "../../support/types/project.types"; import { createProjectIfMissingAPIV2, deleteProjectFromAPIV2, getProjectByNamespaceAPIV2, getUserNamespaceAPIV2, - ProjectIdentifierV2, } from "../../support/utils/projectsV2.utils"; const projectTestConfig = { projectAlreadyExists: false, diff --git a/cypress-tests/cypress/e2e/v2/projectBasics.cy.ts b/cypress-tests/cypress/e2e/v2/projectBasics.cy.ts index e69de29bb..14cbf2007 100644 --- a/cypress-tests/cypress/e2e/v2/projectBasics.cy.ts +++ b/cypress-tests/cypress/e2e/v2/projectBasics.cy.ts @@ -0,0 +1,117 @@ +import { + getRandomString, + getUserData, + validateLoginV2, +} from "../../support/commands/general"; +import { ProjectIdentifierV2 } from "../../support/types/project.types"; +import { User } from "../../support/types/user.types"; +import { + deleteProjectFromAPIV2, + getProjectByNamespaceAPIV2, +} from "../../support/utils/projectsV2.utils"; + +const sessionId = ["projectBasics", getRandomString()]; + +beforeEach(() => { + // Restore the session (login) + cy.session( + sessionId, + () => { + cy.robustLogin(); + }, + validateLoginV2 + ); +}); + +describe("Project - create, edit and delete", () => { + // Define some project details + const projectNameRandomPart = getRandomString(); + const projectName = `project/$test-${projectNameRandomPart}`; + const projectPath = `project-test-${projectNameRandomPart}`; + const projectDescription = "This is a test project from Cypress"; + const projectIdentifier: ProjectIdentifierV2 = { + slug: projectPath, + id: null, + namespace: null, + }; + + // Cleanup the project after the test -- useful on failure + after(() => { + getProjectByNamespaceAPIV2(projectIdentifier).then((response) => { + if (response.status === 200) { + projectIdentifier.id = response.body.id; + projectIdentifier.namespace = response.body.namespace; + deleteProjectFromAPIV2(projectIdentifier); + } + }); + }); + + it("Project - create, edit and delete", () => { + // Create a new project + cy.visit("/v2"); + getUserData().then((user: User) => { + const username = user.username; + cy.getDataCy("navbar-new-entity").click(); + cy.getDataCy("navbar-project-new").click(); + cy.getDataCy("project-creation-form").should("exist"); + cy.getDataCy("project-name-input").type(projectName); + cy.getDataCy("project-slug-toggle").click(); + cy.getDataCy("project-slug-input").should("have.value", projectPath); + cy.getDataCy("project-visibility-public").click(); + cy.getDataCy("project-description-input").type(projectDescription); + cy.getDataCy("project-url-preview").contains( + `/${username}/${projectPath}` + ); + cy.intercept("POST", /(?:\/ui-server)?\/api\/data\/projects/).as("createProject"); + cy.getDataCy("project-create-button").click(); + cy.wait("@createProject"); + cy.getDataCy("project-name").should("contain", projectName); + + // Change settings + const modifiedProjectName = `${projectName} - modified`; + const modifiedProjectDescription = `${projectDescription} - modified`; + cy.getDataCy("project-settings-link").click(); + cy.getDataCy("project-name-input").should("have.value", projectName); + cy.getDataCy("project-name-input").clear().type(modifiedProjectName); + + cy.getDataCy("project-description-input").should( + "have.value", + projectDescription + ); + cy.getDataCy("project-description-input") + .clear() + .type(modifiedProjectDescription); + + cy.get("#project-visibility-public").should("be.checked"); + cy.getDataCy("project-visibility-private").click(); + + cy.intercept("PATCH", /(?:\/ui-server)?\/api\/data\/projects\/[^/]+/).as( + "updateProject" + ); + cy.getDataCy("project-update-button").click(); + cy.wait("@updateProject"); + cy.getDataCy("project-settings-general") + .get(".alert-success") + .contains("The project has been successfully updated."); + cy.getDataCy("project-name").should("contain", modifiedProjectName); + cy.getDataCy("project-description").should( + "contain", + modifiedProjectDescription + ); + + // Delete project + cy.getDataCy("project-settings-link").click(); + cy.getDataCy("project-delete"); + cy.getDataCy("project-delete-button").should("not.be.enabled"); + cy.getDataCy("delete-confirmation-input").type(projectPath); + cy.intercept("DELETE", /(?:\/ui-server)?\/api\/data\/projects\/[^/]+/).as( + "deleteProject" + ); + cy.getDataCy("project-delete-button").should("be.enabled").click(); + cy.wait("@deleteProject"); + getProjectByNamespaceAPIV2(projectIdentifier).then((response) => { + expect(response.status).to.equal(404); + }); + }); + }); +}); diff --git a/cypress-tests/cypress/support/commands/general.ts b/cypress-tests/cypress/support/commands/general.ts index 8f73425f3..c816748e6 100644 --- a/cypress-tests/cypress/support/commands/general.ts +++ b/cypress-tests/cypress/support/commands/general.ts @@ -1,4 +1,5 @@ import { TIMEOUTS } from "../../../config"; +import { User } from "../types/user.types"; export const validateLogin = { validate() { @@ -6,7 +7,7 @@ export const validateLogin = { // it sometimes randomly responds with 401 and sometimes with 200 (as expected). This wait period seems to // allow Gitlab to "settle" after the login and properly recognize the token and respond with 200. // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(10_000); + cy.wait(TIMEOUTS.short); // This returns 401 when not properly logged in cy.request("ui-server/api/data/user").its("status").should("eq", 200); // This is how the ui decides the user is logged in @@ -19,6 +20,38 @@ export const validateLogin = { }, }; +export const validateLoginV2 = { + validate() { + cy.request("api/data/user").then((response) => { + expect(response.status).to.eq(200); + + expect(response.body).property("id").to.not.be.empty; + expect(response.body).property("id").to.not.be.null; + expect(response.body).property("username").to.not.be.empty; + expect(response.body).property("username").to.not.be.null; + }); + }, +}; + +export function getUserData(): Cypress.Chainable { + return cy.request("api/data/user").as("getUserData").then((response) => { + expect(response.status).to.eq(200); + expect(response.body).property("username").to.exist; + expect(response.body).property("username").to.not.be.empty; + expect(response.body).property("username").to.not.be.null; + + return { + id: response.body.id, + username: response.body.username, + email: response.body.email, + first_name: response.body.first_name, + last_name: response.body.last_name, + is_admin: response.body.is_admin, + } as User; + }); +} + + export const getIframe = (selector: string) => { // https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/blogs__iframes/cypress/support/e2e.js cy.log("getIframeBody"); diff --git a/cypress-tests/cypress/support/commands/login.ts b/cypress-tests/cypress/support/commands/login.ts index 7605bd0c5..268716262 100644 --- a/cypress-tests/cypress/support/commands/login.ts +++ b/cypress-tests/cypress/support/commands/login.ts @@ -1,3 +1,5 @@ +import { TIMEOUTS } from "../../../config"; + const renkuLogin = (credentials: { username: string; password: string }[]) => { cy.wrap(credentials, { log: false }).each( (credential: { password: string; username: string }) => { @@ -102,7 +104,7 @@ function registerAndVerify(props: RegisterAndVerifyProps) { // it sometimes randomly responds with 401 and sometimes with 200 (as expected). This wait period seems to // allow Gitlab to "settle" after the login and properly recognize the token and respond with 200. // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(10_000); + cy.wait(TIMEOUTS.short); cy.request("ui-server/api/data/user").its("status").should("eq", 200); cy.request("ui-server/api/user").then((response) => { expect(response.status).to.eq(200); diff --git a/cypress-tests/cypress/support/types/project.types.ts b/cypress-tests/cypress/support/types/project.types.ts new file mode 100644 index 000000000..53545cac9 --- /dev/null +++ b/cypress-tests/cypress/support/types/project.types.ts @@ -0,0 +1,10 @@ +export type ProjectIdentifierV2 = { + id?: string; + namespace?: string; + slug: string; +}; + +export interface NewProjectV2Body extends ProjectIdentifierV2 { + name: string; + visibility?: "public" | "private"; +} diff --git a/cypress-tests/cypress/support/types/user.types.ts b/cypress-tests/cypress/support/types/user.types.ts new file mode 100644 index 000000000..b3cc4b339 --- /dev/null +++ b/cypress-tests/cypress/support/types/user.types.ts @@ -0,0 +1,8 @@ +export interface User { + id: string; + username: string; + email: string; + first_name: string; + last_name: string; + is_admin: boolean; +} diff --git a/cypress-tests/cypress/support/utils/projectsV2.utils.ts b/cypress-tests/cypress/support/utils/projectsV2.utils.ts index 6d933394d..8de6ace15 100644 --- a/cypress-tests/cypress/support/utils/projectsV2.utils.ts +++ b/cypress-tests/cypress/support/utils/projectsV2.utils.ts @@ -1,13 +1,4 @@ -export type ProjectIdentifierV2 = { - slug: string; - namespace?: string; - id?: string; -}; - -export interface NewProjectV2Props extends ProjectIdentifierV2 { - visibility?: "public" | "private"; - name: string; -} +import { NewProjectV2Body, ProjectIdentifierV2 } from "../types/project.types"; /** Get the namespace of the logged in user from the API. */ export function getUserNamespaceAPIV2(): Cypress.Chainable { @@ -44,14 +35,14 @@ export function getProjectByNamespaceAPIV2( /** Create a project (if the project is missing) by using only the API. */ export function createProjectIfMissingAPIV2( - newProjectProps: NewProjectV2Props, + newProjectBody: NewProjectV2Body, ) { - return getProjectByNamespaceAPIV2(newProjectProps).then((response) => { + return getProjectByNamespaceAPIV2(newProjectBody).then((response) => { if (response.status != 200) { return cy.request({ method: "POST", url: "api/data/projects", - body: newProjectProps, + body: newProjectBody, headers: { "Content-Type": "application/json", },