From a3e9d81784750327bf54e1f628e0c97d1e9eacd4 Mon Sep 17 00:00:00 2001 From: hdinia <33469289+hdinia@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:31:02 +0100 Subject: [PATCH] test(e2e): implement user auth flow with token strategy --- webapp/cypress/cypress.env.json | 5 ++ webapp/cypress/e2e/studies.cy.ts | 72 +++++++++++++++------- webapp/cypress/support/auth.ts | 100 +++++++++++++++++++++++++++++++ webapp/cypress/support/e2e.ts | 35 ++++++++++- 4 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 webapp/cypress/cypress.env.json create mode 100644 webapp/cypress/support/auth.ts diff --git a/webapp/cypress/cypress.env.json b/webapp/cypress/cypress.env.json new file mode 100644 index 0000000000..d86f0498d9 --- /dev/null +++ b/webapp/cypress/cypress.env.json @@ -0,0 +1,5 @@ +{ + "TEST_USER": "testuser", + "TEST_PASSWORD": "testpass", + "AUTH_TOKEN": "" +} diff --git a/webapp/cypress/e2e/studies.cy.ts b/webapp/cypress/e2e/studies.cy.ts index 8d36ed0c09..82eb7c31e4 100644 --- a/webapp/cypress/e2e/studies.cy.ts +++ b/webapp/cypress/e2e/studies.cy.ts @@ -14,16 +14,28 @@ 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(() => { + // Mock studies API response with auth header + 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", () => { @@ -48,23 +60,43 @@ describe("Studies Page", () => { }); 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"); + // Re-mock with delay for this specific test + 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.get('[data-testid="loading-spinner"]').should("exist"); }); it("should handle error state", () => { - // Force API error - cy.intercept("GET", "/v1/studies", { - forceNetworkError: true, - }).as("errorStudies"); + // Re-mock with error for this specific test + cy.intercept( + { + method: "GET", + url: "/v1/studies", + headers: { + Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`, + }, + }, + { + forceNetworkError: true, + }, + ).as("errorStudies"); + cy.visit("/studies"); cy.contains("Failed to load studies").should("exist"); cy.get("button").contains("Refresh").should("exist"); }); diff --git a/webapp/cypress/support/auth.ts b/webapp/cypress/support/auth.ts new file mode 100644 index 0000000000..0330405c64 --- /dev/null +++ b/webapp/cypress/support/auth.ts @@ -0,0 +1,100 @@ +/** + * 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 "@testing-library/cypress/add-commands"; + +declare global { + namespace Cypress { + interface Chainable { + /** + * Full authentication flow with proper user creation and login validation + * + * @example cy.login() + */ + login(username?: string, password?: string): Chainable; + } + } +} + +Cypress.Commands.add( + "login", + ( + username = Cypress.env("AUTH_USER") || "testuser", + password = Cypress.env("AUTH_PASSWORD") || "testpass", + ) => { + cy.session([username, password], () => { + // 1. Verify/Create test user + cy.request({ + method: "POST", + url: "/v1/users", + body: { + name: username, + password: password, + }, + failOnStatusCode: false, + }).then((userResponse) => { + if (userResponse.status === 401) { + throw new Error("User creation failed: Unauthorized"); + } + + if (userResponse.status === 409) { + cy.log("User already exists, proceeding with login"); + } else if (userResponse.status >= 400) { + throw new Error(`User creation failed: ${userResponse.body.detail}`); + } + }); + + // 2. Login with credentials + cy.request({ + method: "POST", + url: "/v1/login", + body: { + name: username, + password: password, + }, + failOnStatusCode: false, + }).then((loginResponse) => { + if (loginResponse.status !== 200) { + throw new Error(`Login failed: ${loginResponse.body.detail}`); + } + + // 3. Create bot token with authenticated session + const cookies = loginResponse.headers["set-cookie"]; + cy.request({ + method: "POST", + url: "/v1/bots", + headers: { + Cookie: Array.isArray(cookies) ? cookies.join("; ") : cookies || "", + "Content-Type": "application/json", + }, + body: { + name: `cypress-bot-${Date.now()}`, + roles: [{ group: "cypress-tests", role: 0 }], + is_author: true, + }, + }).then((botResponse) => { + window.localStorage.setItem("authToken", botResponse.body.token); + Cypress.env("AUTH_TOKEN", botResponse.body.token); + }); + }); + + // 4. Validate UI flow + cy.visit("/"); + cy.get('input[name="name"]').type(username); + cy.get('input[name="password"]').type(password, { log: false }); + cy.get('button[type="submit"]').click(); + cy.url().should("include", "/studies"); + }); + }, +); diff --git a/webapp/cypress/support/e2e.ts b/webapp/cypress/support/e2e.ts index 59d8a0b433..d42f420720 100644 --- a/webapp/cypress/support/e2e.ts +++ b/webapp/cypress/support/e2e.ts @@ -12,16 +12,45 @@ * This file is part of the Antares project. */ +import "./auth"; import "@testing-library/cypress/add-commands"; +const AppPages = { + studies: "/studies", + settings: "/settings", +} as const; + declare global { namespace Cypress { interface Chainable { - login(username: string, password: string): Chainable; + /** + * Navigate to a specific page in the application + * + * @example cy.navigateTo('studies') + */ + navigateTo(page: keyof typeof AppPages): Chainable; + + /** + * Check main application header visibility + * + * @example cy.checkHeader() + */ + checkHeader(): Chainable; } } } -Cypress.Commands.add("login", (username, password) => { - // TODO: add login command logic +Cypress.Commands.add("navigateTo", (page) => { + const path = AppPages[page]; + + cy.location("pathname").then((currentPath) => { + if (currentPath !== path) { + cy.visit(path); + } + }); +}); + +// Global intercepts for common API calls +beforeEach(() => { + cy.intercept("GET", "/v1/health", { statusCode: 200 }).as("healthCheck"); });