Skip to content

Commit

Permalink
test: add a cypress test for project basic operations: create, edit, …
Browse files Browse the repository at this point in the history
…delete (#3881)
  • Loading branch information
lorenzo-cavazzi authored Jan 31, 2025
1 parent cbe29dc commit b0a62ab
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 16 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pull-request-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ jobs:
checkWorkflows,
rstudioSession,
v2/dashboardV2,
v2/projectBasics,
]

steps:
Expand Down
2 changes: 1 addition & 1 deletion cypress-tests/cypress/e2e/v2/dashboardV2.cy.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
117 changes: 117 additions & 0 deletions cypress-tests/cypress/e2e/v2/projectBasics.cy.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
35 changes: 34 additions & 1 deletion cypress-tests/cypress/support/commands/general.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { TIMEOUTS } from "../../../config";
import { User } from "../types/user.types";

export const validateLogin = {
validate() {
// If we send a request to the user endpoint on Gitlab too quickly after we log in then
// 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
Expand All @@ -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<User> {
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");
Expand Down
4 changes: 3 additions & 1 deletion cypress-tests/cypress/support/commands/login.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions cypress-tests/cypress/support/types/project.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type ProjectIdentifierV2 = {
id?: string;
namespace?: string;
slug: string;
};

export interface NewProjectV2Body extends ProjectIdentifierV2 {
name: string;
visibility?: "public" | "private";
}
8 changes: 8 additions & 0 deletions cypress-tests/cypress/support/types/user.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface User {
id: string;
username: string;
email: string;
first_name: string;
last_name: string;
is_admin: boolean;
}
17 changes: 4 additions & 13 deletions cypress-tests/cypress/support/utils/projectsV2.utils.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
Expand Down Expand Up @@ -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",
},
Expand Down

0 comments on commit b0a62ab

Please sign in to comment.