mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
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:
@@ -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 { testA11y } from 'cypress/support/utils';
|
||||||
|
import '../support/commands';
|
||||||
|
|
||||||
describe('Site Statistics Page', () => {
|
describe('Site Statistics Page', () => {
|
||||||
it('should load if you click on "Statistics" from homepage', () => {
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
@@ -9,6 +10,10 @@ describe('Site Statistics Page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
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');
|
cy.visit('/statistics');
|
||||||
|
|
||||||
// <ds-site-statistics-page> tag must be visable
|
// <ds-site-statistics-page> tag must be visable
|
||||||
|
@@ -4,10 +4,12 @@
|
|||||||
// ***********************************************
|
// ***********************************************
|
||||||
|
|
||||||
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
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
|
// 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()'.
|
// 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_BASE_URL = 'http://localhost:8080/server';
|
||||||
|
export const FALLBACK_TEST_REST_DOMAIN = 'localhost';
|
||||||
|
|
||||||
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||||
// ALL custom commands MUST be listed here for code completion to work
|
// ALL custom commands MUST be listed here for code completion to work
|
||||||
@@ -30,6 +32,15 @@ declare global {
|
|||||||
* @param password password to login as
|
* @param password password to login as
|
||||||
*/
|
*/
|
||||||
loginViaForm(email: string, password: string): typeof loginViaForm;
|
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,52 +67,57 @@ function login(email: string, password: string): void {
|
|||||||
if (!config.rest.baseUrl) {
|
if (!config.rest.baseUrl) {
|
||||||
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||||
} else {
|
} 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;
|
baseRestUrl = config.rest.baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// To login via REST, first we have to do a GET to obtain a valid CSRF token
|
// Now find domain of our REST API, again with a fallback.
|
||||||
cy.request( baseRestUrl + '/api/authn/status' )
|
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
|
||||||
.then((response) => {
|
if (!config.rest.host) {
|
||||||
// We should receive a CSRF token returned in a response header
|
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
|
||||||
expect(response.headers).to.have.property('dspace-xsrf-token');
|
} else {
|
||||||
const csrfToken = response.headers['dspace-xsrf-token'];
|
baseDomain = config.rest.host;
|
||||||
|
}
|
||||||
|
|
||||||
// Now, send login POST request including that CSRF token
|
// Create a fake CSRF Token. Set it in the required server-side cookie
|
||||||
cy.request({
|
const csrfToken = 'fakeLoginCSRFToken';
|
||||||
method: 'POST',
|
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
|
||||||
url: baseRestUrl + '/api/authn/login',
|
|
||||||
headers: { 'X-XSRF-TOKEN' : 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.
|
// Now, send login POST request including that CSRF token
|
||||||
const authheader = resp.headers.authorization as string;
|
cy.request({
|
||||||
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
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');
|
||||||
|
|
||||||
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
|
// Initialize our AuthTokenInfo object from the authorization header.
|
||||||
// This ensures the UI will recognize we are logged in on next "visit()"
|
const authheader = resp.headers.authorization as string;
|
||||||
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
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')
|
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||||
Cypress.Commands.add('login', login);
|
Cypress.Commands.add('login', login);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login user via displayed login form
|
* Login user via displayed login form
|
||||||
* @param email email to login as
|
* @param email email to login as
|
||||||
* @param password password 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
|
// Enter email
|
||||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
@@ -111,3 +127,60 @@ Cypress.Commands.add('login', login);
|
|||||||
}
|
}
|
||||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||||
Cypress.Commands.add('loginViaForm', 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);
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import { PostRequest } from '../data/request.models';
|
|||||||
import {
|
import {
|
||||||
XSRF_REQUEST_HEADER,
|
XSRF_REQUEST_HEADER,
|
||||||
XSRF_RESPONSE_HEADER
|
XSRF_RESPONSE_HEADER
|
||||||
} from '../xsrf/xsrf.interceptor';
|
} from '../xsrf/xsrf.constants';
|
||||||
|
|
||||||
describe(`ServerAuthRequestService`, () => {
|
describe(`ServerAuthRequestService`, () => {
|
||||||
let href: string;
|
let href: string;
|
||||||
|
@@ -13,7 +13,7 @@ import {
|
|||||||
XSRF_REQUEST_HEADER,
|
XSRF_REQUEST_HEADER,
|
||||||
XSRF_RESPONSE_HEADER,
|
XSRF_RESPONSE_HEADER,
|
||||||
DSPACE_XSRF_COOKIE
|
DSPACE_XSRF_COOKIE
|
||||||
} from '../xsrf/xsrf.interceptor';
|
} from '../xsrf/xsrf.constants';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
33
src/app/core/xsrf/xsrf.constants.ts
Normal file
33
src/app/core/xsrf/xsrf.constants.ts
Normal 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';
|
@@ -12,15 +12,7 @@ import { Observable, throwError } from 'rxjs';
|
|||||||
import { tap, catchError } from 'rxjs/operators';
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from './xsrf.constants';
|
||||||
// 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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Http Interceptor intercepting Http Requests & Responses to
|
* Custom Http Interceptor intercepting Http Requests & Responses to
|
||||||
|
@@ -9,7 +9,7 @@ import { UploaderOptions } from './uploader-options.model';
|
|||||||
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
|
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
|
||||||
import { UploaderProperties } from './uploader-properties.model';
|
import { UploaderProperties } from './uploader-properties.model';
|
||||||
import { HttpXsrfTokenExtractor } from '@angular/common/http';
|
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 { CookieService } from '../../../core/services/cookie.service';
|
||||||
import { DragService } from '../../../core/drag.service';
|
import { DragService } from '../../../core/drag.service';
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user