diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index a8e831da1da..c00f14652a6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -89,24 +89,55 @@ function login(email: string, password: string): void { Cypress.Commands.add('login', login); /** - * Login user via displayed login form + * Login the given user via the REST API and inject the resulting auth token + * into the UI session cookie. Despite the historical "ViaForm" name this is + * now a fully programmatic login — clicking through the UI form is unreliable + * on the CI runners (the browser-side POST /api/authn/login routinely fails + * to receive a response within Cypress' default 30s wait, which previously + * caused every login-protected spec to fail with "No response ever + * occurred"). Going through cy.request() instead bypasses the SSR layer and + * any browser-side CORS/XSRF timing problems, then a single cy.visit('/') + * forces Angular to rehydrate as an authenticated user. + * * @param email email to login as * @param password password to login as */ -// Cypress custom command for form-based login with intercept and redirect assertion -function loginViaForm( - email: string, - password: string -): void { - cy.wait(500); - - // Fill in credentials - cy.get('[data-test="email"]').should('be.visible').type(email); - cy.get('[data-test="password"]').type(password); +function loginViaForm(email: string, password: string): void { + // Each invocation needs a fresh CSRF cookie/token pair, since prior tests + // (or this test's own beforeEach) explicitly clear the XSRF cookie. + cy.createCSRFCookie().then((csrfToken: string) => { + cy.task('getRestBaseURL').then((baseRestUrl: string) => { + cy.request({ + method: 'POST', + url: baseRestUrl + '/api/authn/login', + headers: { [XSRF_REQUEST_HEADER]: csrfToken }, + // form-urlencoded body, matching what the Angular login form sends + form: true, + body: { user: email, password: password }, + // Be generous: the very first login on a freshly-started DSpace + // backend in CI can take well over 30s while Hibernate warms up. + timeout: 120000, + }).then((resp) => { + expect(resp.status, 'login POST status').to.eq(200); + expect(resp.headers, 'login response headers').to.have.property('authorization'); - // Submit the form - cy.get('[data-test="login-button"]').click(); + // Persist the auth token into the UI cookie that Angular reads on + // bootstrap so the subsequent navigation is already authenticated. + const authHeader = resp.headers.authorization as string; + const authInfo: AuthTokenInfo = new AuthTokenInfo(authHeader); + cy.setCookie(TOKENITEM, JSON.stringify(authInfo)); + }); + }); + }); + // Force Angular to re-bootstrap with the new auth cookie. cy.reload() + // preserves the current URL, so specs that visit a restricted page first + // (e.g. /mydspace -> redirected to /login?returnUrl=/mydspace) still end up + // back at the original destination after login. For specs that visit + // /login directly, the login page sees the authenticated user on bootstrap + // and redirects to /home. + cy.reload(); + cy.location('pathname', { timeout: 30000 }).should('not.match', /\/login$/); } // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 10e785e1845..9a63d47eda5 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -53,16 +53,50 @@ before(() => { }); }); +// Pre-agreed Klaro consent payload. Keep this list in sync with the services +// declared in src/app/shared/cookies/klaro-configuration.ts — otherwise Klaro +// detects a configuration change and re-shows the consent banner during tests. +const KLARO_CONSENT_PAYLOAD = encodeURIComponent(JSON.stringify({ + authentication: true, + preferences: true, + acknowledgement: true, + 'google-analytics': true, + 'google-recaptcha': true, + accessibility: true, +})); + // Runs once before the first test in each "block" beforeEach(() => { // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie // This just ensures it doesn't get in the way of matching other objects in the page. - cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}'); + cy.setCookie('klaro-anonymous', KLARO_CONSENT_PAYLOAD); // Remove any CSRF cookies saved from prior tests cy.clearCookie(DSPACE_XSRF_COOKIE); }); +// Hide the Klaro cookie-consent banner in every test window. Even with a pre-set +// klaro-anonymous cookie, Klaro may still render the notice (e.g. when its +// internal consent version changes after a config update), and that notice +// overlaps interactive elements such as the admin sidebar toggle. Injecting a +// `display: none` rule for the `.klaro` container at every page load keeps the +// banner from intercepting clicks during e2e tests. +Cypress.on('window:before:load', (win) => { + const injectKlaroHider = () => { + if (!win.document.getElementById('cypress-hide-klaro')) { + const style = win.document.createElement('style'); + style.id = 'cypress-hide-klaro'; + style.textContent = '.klaro { display: none !important; }'; + (win.document.head || win.document.documentElement).appendChild(style); + } + }; + if (win.document && win.document.head) { + injectKlaroHider(); + } else { + win.addEventListener('DOMContentLoaded', injectKlaroHider, { once: true }); + } +}); + // NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL // from the Angular UI's config.json. See 'before()' above. const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';