Skip to content

Commit

Permalink
test(e2e): implement user auth flow with token strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
hdinia committed Jan 27, 2025
1 parent 7065686 commit d494952
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 90 deletions.
10 changes: 5 additions & 5 deletions webapp/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@
*/

import { defineConfig } from "cypress";
import path from "path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";

export default defineConfig({
e2e: {
supportFile: "cypress/support/e2e.ts",
specPattern: "cypress/e2e/**/*.cy.{ts,tsx}",
baseUrl: "http://localhost:3000",
viewportWidth: 1280,
viewportHeight: 720,
},
component: {
supportFile: "cypress/support/component.ts",
specPattern: "cypress/component/**/*.cy.{ts,tsx}",
devServer: {
framework: "react",
bundler: "vite",
viteConfig: {
configFile: path.resolve(__dirname, "vite.config.ts"),
configFile: resolve(dirname(fileURLToPath(import.meta.url)), "vite.config.ts"),
},
},
},
Expand Down
3 changes: 3 additions & 0 deletions webapp/cypress/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
screenshots
videos
downloads
18 changes: 18 additions & 0 deletions webapp/cypress/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

export const AppPages = {
studies: "/studies",
settings: "/settings",
} as const;
6 changes: 6 additions & 0 deletions webapp/cypress/cypress.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"USERNAME": "testuser",
"PASSWORD": "testpass",
"AUTH_TOKEN": null,
"REFRESH_TOKEN": null
}
66 changes: 66 additions & 0 deletions webapp/cypress/e2e/auth.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

describe("Authentication Flow", () => {
describe("Login Page", () => {
beforeEach(() => {
cy.visit("/");
});

it("should not allow submit when form is empty", () => {
cy.findByRole("button", { name: /connexion/i }).should("be.disabled");
});

it("should show error when using invalid credentials", () => {
// Fill form with invalid credentials
cy.findByLabelText(/nni/i).type("invalid_user");
// TODO: fix the accessibility issue, and replace the css selector by "cy-data" or use testing lib API.
cy.get('input[name="password"]').type("invalid_password");

// Submit wrong user
cy.findByRole("button", { name: /connexion/i })
.should("be.enabled")
.click();

// Verify the login attempt fails
// TODO: move css selector to "cy-data" or testing lib API.
cy.get(".notistack-SnackbarContainer").within(() => {
cy.findByText("Error while submitting").should("exist");
// TODO: assertions below fail because of accessibility lack
//cy.findByText("Status: 401").should("exist");
//cy.findByText("Exception: HTTPException").should("exist");
//cy.findByText("Description: Bad username or password").should("exist");
});
});
});

describe("Successful Authentication", () => {
it("should store tokens for subsequent tests", () => {
// Call login command with authenticated user
cy.visit("/");
cy.findByLabelText(/nni/i).type("admin");
// TODO: move css selector to "cy-data" or testing lib API.
cy.get('input[name="password"]').type("admin");
cy.findByRole("button", { name: /connexion/i })
.should("be.enabled")
.click();

cy.login();

// Verify tokens are stored
cy.wrap(Cypress.env("ACCESS_TOKEN")).should("be.a", "string");
cy.wrap(Cypress.env("REFRESH_TOKEN")).should("be.a", "string");
});
});
});
95 changes: 55 additions & 40 deletions webapp/cypress/e2e/studies.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,73 @@

describe("Studies Page", () => {
beforeEach(() => {
// Mock API response for studies
cy.intercept("GET", "/v1/studies", {
statusCode: 200,
body: [
{ id: "1", name: "Study 1", description: "First study" },
{ id: "2", name: "Study 2", description: "Second study" },
],
}).as("getStudies");
cy.login().then(() => {
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
{
statusCode: 200,
body: [
{ id: "1", name: "Study 1", description: "First study" },
{ id: "2", name: "Study 2", description: "Second study" },
],
},
).as("getStudies");

cy.visit("/studies");
cy.visit("/studies");
cy.wait("@getStudies");
});
});

it("should display the studies page correctly", () => {
// Check page title
cy.get("h1").should("contain", "Studies");

// Check header elements
cy.get("header").should("contain", "Global Studies");
cy.get('button[aria-label="Filter"]').should("exist");

// Check side navigation
cy.get("nav").within(() => {
cy.contains("All Studies").should("exist");
cy.contains("Favorites").should("exist");
});
cy.findByRole("heading", { name: /studies/i }).should("exist");

// Verify studies list
cy.get('[data-testid="studies-list"]').within(() => {
cy.contains("Study 1").should("exist");
cy.contains("Study 2").should("exist");
cy.findByRole("list", { name: /studies list/i }).within(() => {
cy.findByText("Study 1").should("exist");
cy.findByText("Study 2").should("exist");
});
});

it("should handle loading state", () => {
// Delay the API response to test loading state
cy.intercept("GET", "/v1/studies", (req) => {
req.reply({
delay: 1000,
body: [],
});
}).as("delayedStudies");

cy.get('[data-testid="loading-spinner"]').should("exist");
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
(req) => {
req.reply({
delay: 1000,
body: [],
});
},
).as("delayedStudies");
cy.visit("/studies");
cy.findByRole("status", { name: /loading/i }).should("exist");
});

it("should handle error state", () => {
// Force API error
cy.intercept("GET", "/v1/studies", {
forceNetworkError: true,
}).as("errorStudies");
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
{
forceNetworkError: true,
},
).as("errorStudies");

cy.contains("Failed to load studies").should("exist");
cy.get("button").contains("Refresh").should("exist");
cy.visit("/studies");
cy.findByRole("alert", { name: /error/i }).should("contain", "Failed to load studies");
});
});
Empty file.
14 changes: 0 additions & 14 deletions webapp/cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,4 @@
import { mount } from "cypress/react";
import "@testing-library/cypress/add-commands";

declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}

declare namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}

Cypress.Commands.add("mount", mount);
45 changes: 37 additions & 8 deletions webapp/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,45 @@
* This file is part of the Antares project.
*/

import { AppPages } from "constants";
import "@testing-library/cypress/add-commands";

declare global {
namespace Cypress {
interface Chainable {
login(username: string, password: string): Chainable<void>;
Cypress.Commands.add(
"login",
(username = Cypress.env("USERNAME"), password = Cypress.env("PASSWORD")) => {
cy.session([username, password], () => {
cy.request({
method: "POST",
url: "/v1/login",
body: { username, password },
headers: {
"Content-Type": "application/json",
},
}).then(({ body }) => {
// Store tokens in env vars
Cypress.env("ACCESS_TOKEN", body.access_token);
Cypress.env("REFRESH_TOKEN", body.refresh_token);
});
});

// Verify successful login
cy.visit("/");
cy.findByRole("heading", { name: /antares web/i }).should("exist");
cy.url().should("include", "/studies");
},
);

Cypress.Commands.add("navigateTo", (page) => {
const path = AppPages[page];

cy.location("pathname").then((currentPath) => {
if (currentPath !== path) {
cy.visit(path);
}
}
}
});
});

Cypress.Commands.add("login", (username, password) => {
// TODO: add login command logic
// Global intercepts for common API calls
beforeEach(() => {
cy.intercept("GET", "/v1/health", { statusCode: 200 }).as("healthCheck");
});
6 changes: 4 additions & 2 deletions webapp/cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress"],
"baseUrl": "..",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["**/*.ts"]
"include": [
"**/*.ts",
]
}
45 changes: 45 additions & 0 deletions webapp/cypress/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

// cypress/types/global.d.ts
import { TAppPages } from "./index";
import { mount } from "cypress/react";

declare global {
namespace Cypress {
interface Chainable {
/**
* Full authentication flow using API calls and Testing Library
*
* @example cy.login() // Uses env vars
* @example cy.login('customUser', 'securePass')
*/
login(username?: string, password?: string): Chainable<void>;

/**
* Navigate to a specific page in the application
*
* @example cy.navigateTo('studies')
*/
navigateTo(page: TAppPages): Chainable<void>;

/**
* Mount component for testing
*
* @example cy.mount(<MyComponent />)
*/
mount: typeof mount;
}
}
}
17 changes: 17 additions & 0 deletions webapp/cypress/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import type { AppPages } from "constants";

export type TAppPages = keyof typeof AppPages;
Loading

0 comments on commit d494952

Please sign in to comment.