dceky 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ declare global {
|
|
|
8
8
|
namespace Cypress {
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
|
-
* Handle a HarvardKey login page for a user
|
|
11
|
+
* Handle a HarvardKey login page for a user via UI (form filling with cy.origin(), not API calls)
|
|
12
12
|
* @author Yuen Ler Chow
|
|
13
13
|
* @param name the name of the user environment variable
|
|
14
14
|
* @return Cypress chainable (void) - performs authentication flow, no return value
|
|
@@ -30,16 +30,15 @@ const handleHarvardKey = () => {
|
|
|
30
30
|
(
|
|
31
31
|
name: string,
|
|
32
32
|
) => {
|
|
33
|
-
cy.log('🔓 Handling HarvardKey authentication');
|
|
33
|
+
cy.log('🔓 Handling HarvardKey authentication via UI');
|
|
34
34
|
|
|
35
35
|
const userInfo = Cypress.env(name);
|
|
36
36
|
if (!userInfo) {
|
|
37
37
|
throw new Error(`Could not find ${name} in environment variables`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// Destructure the user info object
|
|
41
|
-
const { username } = userInfo;
|
|
42
|
-
const { password } = userInfo;
|
|
40
|
+
// Destructure the user info object
|
|
41
|
+
const { username, password } = userInfo;
|
|
43
42
|
|
|
44
43
|
// Get the Harvard login URL using originWithKaixa with auto-initialized functions
|
|
45
44
|
cy.origin('https://apps.cirrusidentity.com', () => {
|
package/src/commands/index.ts
CHANGED
|
@@ -18,13 +18,14 @@ import handleHarvardKey from './handleHarvardKey';
|
|
|
18
18
|
import launchAs from './launchAs';
|
|
19
19
|
import listSelectLabels from './listSelectLabels';
|
|
20
20
|
import listSelectValues from './listSelectValues';
|
|
21
|
+
import logIntoPorta from './logIntoPorta';
|
|
21
22
|
import navigateToHref from './navigateToHref';
|
|
22
23
|
import padWithZeros from './padWithZeros';
|
|
23
24
|
import runScript from './runScript';
|
|
24
|
-
import typeInto from './typeInto';
|
|
25
|
-
import uniquify from './uniquify';
|
|
26
25
|
import tap from './tap';
|
|
27
26
|
import tapInIFrame from './tapInIFrame';
|
|
27
|
+
import typeInto from './typeInto';
|
|
28
|
+
import uniquify from './uniquify';
|
|
28
29
|
import visitCanvasEndpoint from './visitCanvasEndpoint';
|
|
29
30
|
import waitForAtLeastOneElementPresent from './waitForAtLeastOneElementPresent';
|
|
30
31
|
import waitForElementVisible from './waitForElementVisible';
|
|
@@ -54,13 +55,14 @@ const commands = () => {
|
|
|
54
55
|
launchAs();
|
|
55
56
|
listSelectLabels();
|
|
56
57
|
listSelectValues();
|
|
58
|
+
logIntoPorta();
|
|
57
59
|
navigateToHref();
|
|
58
60
|
padWithZeros();
|
|
59
61
|
runScript();
|
|
60
|
-
typeInto();
|
|
61
|
-
uniquify();
|
|
62
62
|
tap();
|
|
63
63
|
tapInIFrame();
|
|
64
|
+
typeInto();
|
|
65
|
+
uniquify();
|
|
64
66
|
visitCanvasEndpoint();
|
|
65
67
|
waitForAtLeastOneElementPresent();
|
|
66
68
|
waitForElementVisible();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
|
|
3
|
+
/*----------------------------------------*/
|
|
4
|
+
/* ---------------- Type ---------------- */
|
|
5
|
+
/*----------------------------------------*/
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
namespace Cypress {
|
|
9
|
+
interface Chainable {
|
|
10
|
+
/**
|
|
11
|
+
* Log into Porta via HarvardKey authentication using API requests (no UI interaction).
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. GET Cirrus Identity discovery page → extract HarvardKey IdP button href
|
|
15
|
+
* 2. GET IdP login page → extract form action and hidden fields
|
|
16
|
+
* 3. POST credentials to the login form
|
|
17
|
+
* 4. POST SAML form to ACS
|
|
18
|
+
*
|
|
19
|
+
* @author Yuen Ler Chow
|
|
20
|
+
* @param name the name of the user environment variable
|
|
21
|
+
* @return Cypress chainable (void) - performs authentication flow, no return value
|
|
22
|
+
*/
|
|
23
|
+
loginToPorta(
|
|
24
|
+
name: string,
|
|
25
|
+
): Chainable<void>;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*----------------------------------------*/
|
|
31
|
+
/* ---------------- Constants ----------------- */
|
|
32
|
+
/*----------------------------------------*/
|
|
33
|
+
|
|
34
|
+
// Cirrus Identity discovery page (Request 1)
|
|
35
|
+
const LOGIN_PAGE = 'https://harvard.proxy.cirrusidentity.com/cas/login?service=https%3A%2F%2Fporta-auto.dcex.harvard.edu%2Fcas%2Flogin%2F';
|
|
36
|
+
// Base URL for HarvardKey IdP (Request 2)
|
|
37
|
+
const IDP_BASE_URL = 'https://apps.cirrusidentity.com';
|
|
38
|
+
// HarvardKey IdP button ID on Cirrus discovery page
|
|
39
|
+
const HARVARDKEY_IDP_BUTTON_ID = 'idp_1001962798_button';
|
|
40
|
+
|
|
41
|
+
/*----------------------------------------*/
|
|
42
|
+
/* -------------- Helpers --------------- */
|
|
43
|
+
/*----------------------------------------*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse HTML and extract form action URL and hidden input fields.
|
|
47
|
+
* @author Yuen Ler Chow
|
|
48
|
+
* @param html - Raw HTML string to parse
|
|
49
|
+
* @param formSelector - CSS selector for the form (default: 'form')
|
|
50
|
+
* @returns Object with action URL and record of hidden field name/value pairs
|
|
51
|
+
*/
|
|
52
|
+
const getFormData = (html: string, formSelector = 'form') => {
|
|
53
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
54
|
+
const form = doc.querySelector(formSelector);
|
|
55
|
+
const action = form?.getAttribute('action') ?? '';
|
|
56
|
+
const fields: { [key: string]: string } = {};
|
|
57
|
+
form?.querySelectorAll('input[type=hidden]').forEach((input) => {
|
|
58
|
+
const name = input.getAttribute('name');
|
|
59
|
+
if (name) fields[name] = input.getAttribute('value') ?? '';
|
|
60
|
+
});
|
|
61
|
+
return { action, fields };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/*----------------------------------------*/
|
|
65
|
+
/* --------------- Command -------------- */
|
|
66
|
+
/*----------------------------------------*/
|
|
67
|
+
|
|
68
|
+
const loginToPorta = () => {
|
|
69
|
+
Cypress.Commands.add(
|
|
70
|
+
'loginToPorta',
|
|
71
|
+
(
|
|
72
|
+
name: string,
|
|
73
|
+
) => {
|
|
74
|
+
cy.log('🔓 Handling HarvardKey authentication via API');
|
|
75
|
+
|
|
76
|
+
const userInfo = Cypress.env(name);
|
|
77
|
+
if (!userInfo) {
|
|
78
|
+
throw new Error(`Could not find ${name} in environment variables`);
|
|
79
|
+
}
|
|
80
|
+
const { username, password } = userInfo;
|
|
81
|
+
|
|
82
|
+
// Check if username and password are set
|
|
83
|
+
if (!username || !password) {
|
|
84
|
+
throw new Error(`Credential "${name}" is missing username and/or password`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const htmlHeaders = { accept: 'text/html' };
|
|
88
|
+
|
|
89
|
+
// Request 1: GET Cirrus Identity discovery page
|
|
90
|
+
cy.request({
|
|
91
|
+
url: LOGIN_PAGE,
|
|
92
|
+
followRedirect: true,
|
|
93
|
+
failOnStatusCode: false,
|
|
94
|
+
headers: htmlHeaders,
|
|
95
|
+
}).then((discoveryRes) => {
|
|
96
|
+
// Parse discovery page for HarvardKey IdP button href
|
|
97
|
+
const btnMatch = discoveryRes.body.match(
|
|
98
|
+
new RegExp(`href=["']([^"']+)["'][^>]*id=["']${HARVARDKEY_IDP_BUTTON_ID}["']`),
|
|
99
|
+
);
|
|
100
|
+
if (!btnMatch) {
|
|
101
|
+
throw new Error('Could not find HarvardKey IdP button link on discovery page');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const idpUrl = `${IDP_BASE_URL}${btnMatch[1]}`;
|
|
105
|
+
|
|
106
|
+
// Request 2: GET HarvardKey IdP login page
|
|
107
|
+
cy.request({
|
|
108
|
+
url: idpUrl,
|
|
109
|
+
followRedirect: true,
|
|
110
|
+
failOnStatusCode: false,
|
|
111
|
+
headers: htmlHeaders,
|
|
112
|
+
}).then((loginPageRes) => {
|
|
113
|
+
// Parse login form action and hidden fields
|
|
114
|
+
const {
|
|
115
|
+
action: loginFormUrl,
|
|
116
|
+
fields: loginHiddenFields,
|
|
117
|
+
} = getFormData(loginPageRes.body);
|
|
118
|
+
if (!loginFormUrl) {
|
|
119
|
+
throw new Error('Could not find login form action on HarvardKey page');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Request 3: POST credentials to HarvardKey login form
|
|
123
|
+
cy.request({
|
|
124
|
+
method: 'POST',
|
|
125
|
+
url: loginFormUrl,
|
|
126
|
+
form: true,
|
|
127
|
+
followRedirect: true,
|
|
128
|
+
failOnStatusCode: false,
|
|
129
|
+
headers: htmlHeaders,
|
|
130
|
+
body: {
|
|
131
|
+
...loginHiddenFields,
|
|
132
|
+
username,
|
|
133
|
+
password,
|
|
134
|
+
},
|
|
135
|
+
}).then((authRes) => {
|
|
136
|
+
// Parse SAML form from login response
|
|
137
|
+
const {
|
|
138
|
+
action: samlFormUrl,
|
|
139
|
+
fields: samlHiddenFields,
|
|
140
|
+
} = getFormData(authRes.body, 'form[action*="saml2-acs"]');
|
|
141
|
+
if (!samlFormUrl) {
|
|
142
|
+
throw new Error('Could not find SAML form in login response');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Request 4: POST SAML assertion to ACS
|
|
146
|
+
return cy.request({
|
|
147
|
+
method: 'POST',
|
|
148
|
+
url: samlFormUrl,
|
|
149
|
+
form: true,
|
|
150
|
+
followRedirect: true,
|
|
151
|
+
failOnStatusCode: false,
|
|
152
|
+
headers: htmlHeaders,
|
|
153
|
+
body: samlHiddenFields,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/*----------------------------------------*/
|
|
163
|
+
/* --------------- Export --------------- */
|
|
164
|
+
/*----------------------------------------*/
|
|
165
|
+
|
|
166
|
+
export default loginToPorta;
|