Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cypress test for testing project basic operations: create, edit, delete #3881

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
leafty marked this conversation as resolved.
Show resolved Hide resolved
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();
leafty marked this conversation as resolved.
Show resolved Hide resolved
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();
leafty marked this conversation as resolved.
Show resolved Hide resolved
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
Loading