// *********************************************** // This File is for Custom Cypress commands. // See docs at https://docs.cypress.io/api/cypress-api/custom-commands // *********************************************** import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants'; // 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 'login()'. export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; export const FALLBACK_TEST_REST_DOMAIN = 'localhost'; // Declare Cypress namespace to help with Intellisense & code completion in IDEs // ALL custom commands MUST be listed here for code completion to work declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { /** * Login to backend before accessing the next page. Ensures that the next * call to "cy.visit()" will be authenticated as this user. * @param email email to login as * @param password password to login as */ login(email: string, password: string): typeof login; /** * Login via form before accessing the next page. Useful to fill out login * form when a cy.visit() call is to an a page which requires authentication. * @param email email to login as * @param password password to login as */ loginViaForm(email: string, password: string): typeof loginViaForm; /** * Generate view event for given object. Useful for testing statistics pages with * pre-generated statistics. This just generates a single "hit", but can be called multiple times to * generate multiple hits. * @param uuid UUID of object * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") */ generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent; } } } /** * Login user via REST API directly, and pass authentication token to UI via * the UI's dsAuthInfo cookie. * WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR. * At this time "loginViaForm()" seems more consistent/stable. * @param email email to login as * @param password password to login as */ function login(email: string, password: string): void { // Cypress doesn't have access to the running application in Node.js. // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. // Instead, we'll read our running application's config.json, which contains the configs & // is regenerated at runtime each time the Angular UI application starts up. cy.task('readUIConfig').then((str: string) => { // Parse config into a JSON object const config = JSON.parse(str); // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; if (!config.rest.baseUrl) { console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); } else { //console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl)); baseRestUrl = config.rest.baseUrl; } // Now find domain of our REST API, again with a fallback. let baseDomain = FALLBACK_TEST_REST_DOMAIN; if (!config.rest.host) { console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); } else { baseDomain = config.rest.host; } // Create a fake CSRF Token. Set it in the required server-side cookie const csrfToken = 'fakeLoginCSRFToken'; cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); // Now, send login POST request including that CSRF token cy.request({ method: 'POST', url: baseRestUrl + '/api/authn/login', headers: { [XSRF_REQUEST_HEADER]: csrfToken}, form: true, // indicates the body should be form urlencoded body: { user: email, password: password } }).then((resp) => { // We expect a successful login expect(resp.status).to.eq(200); // We expect to have a valid authorization header returned (with our auth token) expect(resp.headers).to.have.property('authorization'); // Initialize our AuthTokenInfo object from the authorization header. const authheader = resp.headers.authorization as string; const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); // Save our AuthTokenInfo object to our dsAuthInfo UI cookie // This ensures the UI will recognize we are logged in on next "visit()" cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); }); // Remove cookie with fake CSRF token, as it's no longer needed cy.clearCookie(DSPACE_XSRF_COOKIE); }); } // Add as a Cypress command (i.e. assign to 'cy.login') Cypress.Commands.add('login', login); /** * Login user via displayed login form * @param email email to login as * @param password password to login as */ function loginViaForm(email: string, password: string): void { // Enter email cy.get('ds-log-in [data-test="email"]').type(email); // Enter password cy.get('ds-log-in [data-test="password"]').type(password); // Click login button cy.get('ds-log-in [data-test="login-button"]').click(); } // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); /** * Generate statistic view event for given object. Useful for testing statistics pages with * pre-generated statistics. This just generates a single "hit", but can be called multiple times to * generate multiple hits. * * NOTE: This requires that "solr-statistics.autoCommit=false" be set on the DSpace backend * (as it is in our docker-compose-ci.yml used in CI). * Otherwise, by default, new statistical events won't be saved until Solr's autocommit next triggers. * @param uuid UUID of object * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") */ function generateViewEvent(uuid: string, dsoType: string): void { // Cypress doesn't have access to the running application in Node.js. // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. // Instead, we'll read our running application's config.json, which contains the configs & // is regenerated at runtime each time the Angular UI application starts up. cy.task('readUIConfig').then((str: string) => { // Parse config into a JSON object const config = JSON.parse(str); // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; if (!config.rest.baseUrl) { console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); } else { baseRestUrl = config.rest.baseUrl; } // Now find domain of our REST API, again with a fallback. let baseDomain = FALLBACK_TEST_REST_DOMAIN; if (!config.rest.host) { console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); } else { baseDomain = config.rest.host; } // Create a fake CSRF Token. Set it in the required server-side cookie const csrfToken = 'fakeGenerateViewEventCSRFToken'; cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); // Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header cy.request({ method: 'POST', url: baseRestUrl + '/api/statistics/viewevents', headers: { [XSRF_REQUEST_HEADER] : csrfToken, // use a known public IP address to avoid being seen as a "bot" 'X-Forwarded-For': '1.1.1.1', // Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot" 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', }, //form: true, // indicates the body should be form urlencoded body: { targetId: uuid, targetType: dsoType }, }).then((resp) => { // We expect a 201 (which means statistics event was created) expect(resp.status).to.eq(201); }); // Remove cookie with fake CSRF token, as it's no longer needed cy.clearCookie(DSPACE_XSRF_COOKIE); }); } // Add as a Cypress command (i.e. assign to 'cy.generateViewEvent') Cypress.Commands.add('generateViewEvent', generateViewEvent);