Skip to content

Commit a581219

Browse files
committed
Refactor e2e test infrastruction to allow easier way to lookup REST API info and generate CSRF tokens
1 parent 5dad8be commit a581219

3 files changed

Lines changed: 140 additions & 114 deletions

File tree

cypress/plugins/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
const fs = require('fs');
22

3+
// These two global variables are used to store information about the REST API used
4+
// by these e2e tests. They are filled out prior to running any tests in the before()
5+
// method of e2e.ts. They can then be accessed by any tests via the getters below.
6+
let REST_BASE_URL: string;
7+
let REST_DOMAIN: string;
8+
39
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
410
// For more info, visit https://on.cypress.io/plugins-api
511
module.exports = (on, config) => {
@@ -30,6 +36,24 @@ module.exports = (on, config) => {
3036
}
3137

3238
return null;
39+
},
40+
// Save value of REST Base URL, looked up before all tests.
41+
// This allows other tests to use it easily via getRestBaseURL() below.
42+
saveRestBaseURL(url: string) {
43+
return (REST_BASE_URL = url);
44+
},
45+
// Retrieve currently saved value of REST Base URL
46+
getRestBaseURL() {
47+
return REST_BASE_URL ;
48+
},
49+
// Save value of REST Domain, looked up before all tests.
50+
// This allows other tests to use it easily via getRestBaseDomain() below.
51+
saveRestBaseDomain(domain: string) {
52+
return (REST_DOMAIN = domain);
53+
},
54+
// Retrieve currently saved value of REST Domain
55+
getRestBaseDomain() {
56+
return REST_DOMAIN ;
3357
}
3458
});
3559
};

cypress/support/commands.ts

Lines changed: 76 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
77
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
8-
9-
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
10-
// from the Angular UI's config.json. See 'login()'.
11-
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
12-
export const FALLBACK_TEST_REST_DOMAIN = 'localhost';
8+
import { v4 as uuidv4 } from 'uuid';
139

1410
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
1511
// ALL custom commands MUST be listed here for code completion to work
@@ -41,6 +37,13 @@ declare global {
4137
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
4238
*/
4339
generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent;
40+
41+
/**
42+
* Create a new CSRF token and add to required Cookie. CSRF Token is returned
43+
* in chainable in order to allow it to be sent also in required CSRF header.
44+
* @returns Chainable reference to allow CSRF token to also be sent in header.
45+
*/
46+
createCSRFCookie(): Chainable<any>;
4447
}
4548
}
4649
}
@@ -54,59 +57,32 @@ declare global {
5457
* @param password password to login as
5558
*/
5659
function login(email: string, password: string): void {
57-
// Cypress doesn't have access to the running application in Node.js.
58-
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
59-
// Instead, we'll read our running application's config.json, which contains the configs &
60-
// is regenerated at runtime each time the Angular UI application starts up.
61-
cy.task('readUIConfig').then((str: string) => {
62-
// Parse config into a JSON object
63-
const config = JSON.parse(str);
64-
65-
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
66-
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
67-
if (!config.rest.baseUrl) {
68-
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
69-
} else {
70-
//console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl));
71-
baseRestUrl = config.rest.baseUrl;
72-
}
73-
74-
// Now find domain of our REST API, again with a fallback.
75-
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
76-
if (!config.rest.host) {
77-
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
78-
} else {
79-
baseDomain = config.rest.host;
80-
}
81-
82-
// Create a fake CSRF Token. Set it in the required server-side cookie
83-
const csrfToken = 'fakeLoginCSRFToken';
84-
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
85-
86-
// Now, send login POST request including that CSRF token
87-
cy.request({
88-
method: 'POST',
89-
url: baseRestUrl + '/api/authn/login',
90-
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
91-
form: true, // indicates the body should be form urlencoded
92-
body: { user: email, password: password }
93-
}).then((resp) => {
94-
// We expect a successful login
95-
expect(resp.status).to.eq(200);
96-
// We expect to have a valid authorization header returned (with our auth token)
97-
expect(resp.headers).to.have.property('authorization');
98-
99-
// Initialize our AuthTokenInfo object from the authorization header.
100-
const authheader = resp.headers.authorization as string;
101-
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
102-
103-
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
104-
// This ensures the UI will recognize we are logged in on next "visit()"
105-
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
60+
// Create a fake CSRF cookie/token to use in POST
61+
cy.createCSRFCookie().then((csrfToken: string) => {
62+
// get our REST API's base URL, also needed for POST
63+
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
64+
// Now, send login POST request including that CSRF token
65+
cy.request({
66+
method: 'POST',
67+
url: baseRestUrl + '/api/authn/login',
68+
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
69+
form: true, // indicates the body should be form urlencoded
70+
body: { user: email, password: password }
71+
}).then((resp) => {
72+
// We expect a successful login
73+
expect(resp.status).to.eq(200);
74+
// We expect to have a valid authorization header returned (with our auth token)
75+
expect(resp.headers).to.have.property('authorization');
76+
77+
// Initialize our AuthTokenInfo object from the authorization header.
78+
const authheader = resp.headers.authorization as string;
79+
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
80+
81+
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
82+
// This ensures the UI will recognize we are logged in on next "visit()"
83+
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
84+
});
10685
});
107-
108-
// Remove cookie with fake CSRF token, as it's no longer needed
109-
cy.clearCookie(DSPACE_XSRF_COOKIE);
11086
});
11187
}
11288
// Add as a Cypress command (i.e. assign to 'cy.login')
@@ -141,56 +117,53 @@ Cypress.Commands.add('loginViaForm', loginViaForm);
141117
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
142118
*/
143119
function generateViewEvent(uuid: string, dsoType: string): void {
144-
// Cypress doesn't have access to the running application in Node.js.
145-
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
146-
// Instead, we'll read our running application's config.json, which contains the configs &
147-
// is regenerated at runtime each time the Angular UI application starts up.
148-
cy.task('readUIConfig').then((str: string) => {
149-
// Parse config into a JSON object
150-
const config = JSON.parse(str);
120+
// Create a fake CSRF cookie/token to use in POST
121+
cy.createCSRFCookie().then((csrfToken: string) => {
122+
// get our REST API's base URL, also needed for POST
123+
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
124+
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
125+
cy.request({
126+
method: 'POST',
127+
url: baseRestUrl + '/api/statistics/viewevents',
128+
headers: {
129+
[XSRF_REQUEST_HEADER] : csrfToken,
130+
// use a known public IP address to avoid being seen as a "bot"
131+
'X-Forwarded-For': '1.1.1.1',
132+
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
133+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
134+
},
135+
//form: true, // indicates the body should be form urlencoded
136+
body: { targetId: uuid, targetType: dsoType },
137+
}).then((resp) => {
138+
// We expect a 201 (which means statistics event was created)
139+
expect(resp.status).to.eq(201);
140+
});
141+
});
142+
});
143+
}
144+
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
145+
Cypress.Commands.add('generateViewEvent', generateViewEvent);
151146

152-
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
153-
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
154-
if (!config.rest.baseUrl) {
155-
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
156-
} else {
157-
baseRestUrl = config.rest.baseUrl;
158-
}
159147

160-
// Now find domain of our REST API, again with a fallback.
161-
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
162-
if (!config.rest.host) {
163-
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
164-
} else {
165-
baseDomain = config.rest.host;
166-
}
148+
/**
149+
* Can be used by tests to generate a random XSRF/CSRF token and save it to
150+
* the required XSRF/CSRF cookie for usage when sending POST requests or similar.
151+
* The generated CSRF token is returned in a Chainable to allow it to be also sent
152+
* in the CSRF HTTP Header.
153+
* @returns a Cypress Chainable which can be used to get the generated CSRF Token
154+
*/
155+
function createCSRFCookie(): Cypress.Chainable {
156+
// Generate a new token which is a random UUID
157+
const csrfToken: string = uuidv4();
167158

159+
// Save it to our required cookie
160+
cy.task('getRestBaseDomain').then((baseDomain: string) => {
168161
// Create a fake CSRF Token. Set it in the required server-side cookie
169-
const csrfToken = 'fakeGenerateViewEventCSRFToken';
170162
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
171-
172-
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
173-
cy.request({
174-
method: 'POST',
175-
url: baseRestUrl + '/api/statistics/viewevents',
176-
headers: {
177-
[XSRF_REQUEST_HEADER] : csrfToken,
178-
// use a known public IP address to avoid being seen as a "bot"
179-
'X-Forwarded-For': '1.1.1.1',
180-
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
181-
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
182-
},
183-
//form: true, // indicates the body should be form urlencoded
184-
body: { targetId: uuid, targetType: dsoType },
185-
}).then((resp) => {
186-
// We expect a 201 (which means statistics event was created)
187-
expect(resp.status).to.eq(201);
188-
});
189-
190-
// Remove cookie with fake CSRF token, as it's no longer needed
191-
cy.clearCookie(DSPACE_XSRF_COOKIE);
192163
});
193-
}
194-
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
195-
Cypress.Commands.add('generateViewEvent', generateViewEvent);
196164

165+
// return the generated token wrapped in a chainable
166+
return cy.wrap(csrfToken);
167+
}
168+
// Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie')
169+
Cypress.Commands.add('createCSRFCookie', createCSRFCookie);

cypress/support/e2e.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,49 @@ import './commands';
1919
// Import Cypress Axe tools for all tests
2020
// https://github.com/component-driven/cypress-axe
2121
import 'cypress-axe';
22+
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
23+
24+
25+
// Runs once before all tests
26+
before(() => {
27+
// Cypress doesn't have access to the running application in Node.js.
28+
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
29+
// Instead, we'll read our running application's config.json, which contains the configs &
30+
// is regenerated at runtime each time the Angular UI application starts up.
31+
cy.task('readUIConfig').then((str: string) => {
32+
// Parse config into a JSON object
33+
const config = JSON.parse(str);
34+
35+
// Find URL of our REST API & save to global variable via task
36+
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
37+
if (!config.rest.baseUrl) {
38+
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
39+
} else {
40+
baseRestUrl = config.rest.baseUrl;
41+
}
42+
cy.task('saveRestBaseURL', baseRestUrl);
43+
44+
// Find domain of our REST API & save to global variable via task.
45+
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
46+
if (!config.rest.host) {
47+
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
48+
} else {
49+
baseDomain = config.rest.host;
50+
}
51+
cy.task('saveRestBaseDomain', baseDomain);
52+
53+
});
54+
});
2255

2356
// Runs once before the first test in each "block"
2457
beforeEach(() => {
2558
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
2659
// This just ensures it doesn't get in the way of matching other objects in the page.
2760
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}');
28-
});
29-
30-
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
31-
// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
32-
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
33-
/*afterEach(() => {
34-
cy.window().then((win) => {
35-
win.location.href = 'about:blank';
36-
});
37-
});*/
3861

62+
// Remove any CSRF cookies saved from prior tests
63+
cy.clearCookie(DSPACE_XSRF_COOKIE);
64+
});
3965

4066
// Global constants used in tests
4167
// May be overridden in our cypress.json config file using specified environment variables.
@@ -57,7 +83,10 @@ export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLE
5783
export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
5884
export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
5985
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
60-
86+
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
87+
// from the Angular UI's config.json. See 'before()' above.
88+
const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
89+
const FALLBACK_TEST_REST_DOMAIN = 'localhost';
6190

6291
// USEFUL REGEX for testing
6392

0 commit comments

Comments
 (0)