Add generateViewEvent() util to generate stats in e2e tests. Refactored xsrf.interceptor.ts to move constants to a separate file so they can be reused in e2e tests easily.

This commit is contained in:
Tim Donohue
2023-03-06 16:13:43 -06:00
parent 48e95e262d
commit 371bd072b3
7 changed files with 144 additions and 41 deletions

View File

@@ -1,5 +1,6 @@
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
import '../support/commands';
describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => {
@@ -9,6 +10,10 @@ describe('Site Statistics Page', () => {
});
it('should pass accessibility tests', () => {
// generate 2 view events on an Item's page
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item');
cy.visit('/statistics');
// <ds-site-statistics-page> tag must be visable

View File

@@ -4,10 +4,12 @@
// ***********************************************
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
@@ -30,6 +32,15 @@ declare global {
* @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;
}
}
}
@@ -56,22 +67,27 @@ function login(email: string, password: string): void {
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));
//console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl));
baseRestUrl = config.rest.baseUrl;
}
// To login via REST, first we have to do a GET to obtain a valid CSRF token
cy.request( baseRestUrl + '/api/authn/status' )
.then((response) => {
// We should receive a CSRF token returned in a response header
expect(response.headers).to.have.property('dspace-xsrf-token');
const csrfToken = response.headers['dspace-xsrf-token'];
// 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: { 'X-XSRF-TOKEN' : csrfToken},
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password }
}).then((resp) => {
@@ -88,20 +104,20 @@ function login(email: string, password: string): void {
// 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 {
function loginViaForm(email: string, password: string): void {
// Enter email
cy.get('ds-log-in [data-test="email"]').type(email);
// Enter password
@@ -111,3 +127,60 @@ Cypress.Commands.add('login', login);
}
// 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.
* @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},
//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);

View File

@@ -8,7 +8,7 @@ import { PostRequest } from '../data/request.models';
import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER
} from '../xsrf/xsrf.interceptor';
} from '../xsrf/xsrf.constants';
describe(`ServerAuthRequestService`, () => {
let href: string;

View File

@@ -13,7 +13,7 @@ import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER,
DSPACE_XSRF_COOKIE
} from '../xsrf/xsrf.interceptor';
} from '../xsrf/xsrf.constants';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';

View File

@@ -0,0 +1,33 @@
/**
* XSRF / CSRF related constants
*/
/**
* Name of CSRF/XSRF header we (client) may SEND in requests to backend.
* (This is a standard header name for XSRF/CSRF defined by Angular)
*/
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
/**
* Name of CSRF/XSRF header we (client) may RECEIVE in responses from backend
* This header is defined by DSpace backend, see https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
*/
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
/**
* Name of client-side Cookie where we store the CSRF/XSRF token between requests.
* This cookie is only available to client, and should be updated whenever a new XSRF_RESPONSE_HEADER
* is found in a response from the backend.
*/
export const XSRF_COOKIE = 'XSRF-TOKEN';
/**
* Name of server-side cookie the backend expects the XSRF token to be in.
* When the backend receives a modifying request, it will validate the CSRF/XSRF token by looking
* for a match between the XSRF_REQUEST_HEADER and this Cookie. For more details see
* https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
*
* NOTE: This Cookie is NOT readable to the client/UI. It is only readable to the backend and will
* be sent along automatically by the user's browser.
*/
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';

View File

@@ -12,15 +12,7 @@ import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { CookieService } from '../services/cookie.service';
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
// Name of XSRF header we may receive in responses from backend
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
// Name of cookie where we store the XSRF token
export const XSRF_COOKIE = 'XSRF-TOKEN';
// Name of cookie the backend expects the XSRF token to be in
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from './xsrf.constants';
/**
* Custom Http Interceptor intercepting Http Requests & Responses to

View File

@@ -9,7 +9,7 @@ import { UploaderOptions } from './uploader-options.model';
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
import { UploaderProperties } from './uploader-properties.model';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.constants';
import { CookieService } from '../../../core/services/cookie.service';
import { DragService } from '../../../core/drag.service';