Skip to content

Commit 6d975bc

Browse files
tnorlingCopilot
andauthored
Upgrade/Rollback E2E Tests (#8017)
This pull request introduces a new end-to-end (E2E) test setup for the `ExpressSample` in the MSAL browser samples, along with several improvements to the sample app's authentication flow, version switching, and UI. The most significant changes are the addition of E2E test infrastructure, enhancements to how authentication and profile data are handled and displayed, and improvements to the version switching logic and UI. **E2E Test Infrastructure and Configuration:** * Added E2E test configuration and helpers for `ExpressSample`, including a new `.env.e2e` file, a `jest.config.cjs` configuration, and a `test-helpers.ts` utility for Puppeteer-based tests. This enables automated browser testing of authentication scenarios. [[1]](diffhunk://#diff-d257013e504cdeb26f2bfad2e809b36492f6fdbfc45ffcbf4bc55b2da4887c74R1-R5) [[2]](diffhunk://#diff-b70997a8822f049844c4b3afbe1ab62688a13f909a880256d0c8364fff169c3eR1-R8) [[3]](diffhunk://#diff-a45554c7aba0df12b0d4ff639ce1fe74f9e12a2904ecb1d0d2edbe002bc4eefeR1-R112) * Updated the pipeline configuration to include `ExpressSample` in E2E test runs. * Added necessary devDependencies and scripts for E2E testing in `package.json`. **Authentication and Profile Data Handling:** * Modified the authentication flow to return the full MSAL authentication response, not just the access token, and updated the MS Graph call logic to pass and display this data. [[1]](diffhunk://#diff-aedaa17dbc1e25e5997a95b1bea0c9c3b5a9caa6fbbc943c700972966e16b530L115-R115) [[2]](diffhunk://#diff-57185e771678ea1756556bb0a2d9f5cba1e5441609c75dc66cebf0106e92ce3bL11-R15) [[3]](diffhunk://#diff-57185e771678ea1756556bb0a2d9f5cba1e5441609c75dc66cebf0106e92ce3bL31-R30) [[4]](diffhunk://#diff-57185e771678ea1756556bb0a2d9f5cba1e5441609c75dc66cebf0106e92ce3bL49-R50) [[5]](diffhunk://#diff-57185e771678ea1756556bb0a2d9f5cba1e5441609c75dc66cebf0106e92ce3bR94-R117) * Added UI logic to display both profile data and raw authentication data, with sensitive fields (like the access token) redacted for display. **Version Switching and UI Improvements:** * Refactored version switching logic to use consistent keys (e.g., `latest-v3` instead of `3.x`) and improved the UI for version selection, including unique IDs for dropdown items and better feedback when switching versions. [[1]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL60-R57) [[2]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL70-R74) [[3]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL107-R104) [[4]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL129-R147) [[5]](diffhunk://#diff-2d7f351e21c92ea9ff545acdd7d5094e73a972fe350dea4df139f4e760a77d2eR92) [[6]](diffhunk://#diff-2d7f351e21c92ea9ff545acdd7d5094e73a972fe350dea4df139f4e760a77d2eR111) * Ensured UI elements (such as sign-in and version switcher buttons) are only displayed after event handlers are registered, preventing flicker or premature interaction. [[1]](diffhunk://#diff-83705d1d56435a3dea7f60a3b8bfeb237387c901e34b0a7990233797b9ad2fbaR47-R48) [[2]](diffhunk://#diff-2d7f351e21c92ea9ff545acdd7d5094e73a972fe350dea4df139f4e760a77d2eR64-R66) **UI and Styling Enhancements:** * Updated CSS classes and selectors to use `.json-content` for displaying JSON data, improving clarity and maintainability. [[1]](diffhunk://#diff-0587667bae35b440adac264532b10d1e1c50fd1ad1cf003c42cbceb8365bac54L251-R252) [[2]](diffhunk://#diff-0587667bae35b440adac264532b10d1e1c50fd1ad1cf003c42cbceb8365bac54L260-R261) * Minor CSS and code style cleanups for readability and consistency. [[1]](diffhunk://#diff-0587667bae35b440adac264532b10d1e1c50fd1ad1cf003c42cbceb8365bac54L175-R176) [[2]](diffhunk://#diff-0587667bae35b440adac264532b10d1e1c50fd1ad1cf003c42cbceb8365bac54L523-R530) **Other Improvements:** * Removed unnecessary dependencies (like `morgan`) from the server and cleaned up middleware usage. [[1]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL8) [[2]](diffhunk://#diff-7bf33d829c4c8178361e7badec48a371762d82042e849f0d2c94a2bb41001c8cL39-L40) * Improved screenshot utility to always capture full-page screenshots during E2E tests. These changes collectively improve the testability, usability, and maintainability of the `ExpressSample` app and its E2E testing setup. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 8316ee5 commit 6d975bc

File tree

19 files changed

+2461
-331
lines changed

19 files changed

+2461
-331
lines changed

.pipelines/3p-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ extends:
7777
- "onPageLoad"
7878
- "pop"
7979
- "customizable-e2e-test"
80+
- "ExpressSample"
8081
debug: ${{ parameters.debug }}
8182
npmInstallTimeout: ${{ parameters.npmInstallTimeout }}
8283
- ${{ if eq(parameters.runNodeTests, true) }}:

package-lock.json

Lines changed: 1860 additions & 151 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/e2eTestUtils/src/TestUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export class Screenshot {
2626
await page.screenshot({
2727
path: `${this.folderName}/${++this
2828
.screenshotNum}_${screenshotName}.png`,
29+
fullPage: true
2930
});
3031
}
3132
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CLIENT_ID=b5c2e510-4a17-4feb-b219-e55aa5b74144
2+
AUTHORITY=https://login.microsoftonline.com/common
3+
REDIRECT_URI=http://localhost:3000
4+
POST_LOGOUT_REDIRECT_URI=http://localhost:3000
5+
CACHE_LOCATION=localStorage
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
displayName: "ExpressSample",
3+
globals: {
4+
__PORT__: 3000,
5+
__STARTCMD__: "env-cmd -f .env.e2e npm start",
6+
},
7+
preset: "../../e2eTestUtils/jest-puppeteer-utils/jest-preset.js",
8+
};

samples/msal-browser-samples/ExpressSample/package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,28 @@
66
"scripts": {
77
"start": "node server.js",
88
"dev": "nodemon server.js",
9+
"test:e2e": "jest",
910
"build:package": "npm run build:all --workspace=lib/msal-browser"
1011
},
1112
"dependencies": {
1213
"@azure/msal-browser": "^4.0.0",
14+
"dotenv": "^16.0.3",
1315
"express": "^4.18.2",
14-
"express-handlebars": "^7.0.7",
15-
"morgan": "^1.10.0",
16-
"dotenv": "^16.0.3"
16+
"express-handlebars": "^7.0.7"
1717
},
1818
"devDependencies": {
19-
"nodemon": "^2.0.22"
19+
"@types/jest": "^30.0.0",
20+
"e2e-test-utils": "file:../../e2eTestUtils",
21+
"env-cmd": "^10.1.0",
22+
"jest": "^30.0.5",
23+
"jest-junit": "^16.0.0",
24+
"nodemon": "^2.0.22",
25+
"ts-jest": "^29.4.1"
26+
},
27+
"jest-junit": {
28+
"suiteNameTemplate": "Express Sample Tests",
29+
"outputDirectory": ".",
30+
"outputName": "test-results.xml"
2031
},
2132
"keywords": [
2233
"msal",

samples/msal-browser-samples/ExpressSample/public/css/styles.css

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ header {
2323
background-color: #0078d4;
2424
color: white;
2525
padding: 1rem 0;
26-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
26+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2727
}
2828

2929
.header-content {
@@ -99,7 +99,7 @@ main {
9999
border-radius: 8px;
100100
padding: 2rem;
101101
margin-bottom: 2rem;
102-
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
102+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
103103
}
104104

105105
.card-header {
@@ -172,7 +172,8 @@ main {
172172
border: 1px solid #f5c6cb;
173173
color: #721c24;
174174
position: relative;
175-
padding-right: 2.5rem; /* Make room for dismiss button */
175+
padding-right: 2.5rem;
176+
/* Make room for dismiss button */
176177
}
177178

178179
.error-message a {
@@ -248,7 +249,7 @@ main {
248249
word-break: break-word;
249250
}
250251

251-
.profile-json {
252+
.json-content {
252253
background-color: #f8f9fa;
253254
border: 1px solid #dee2e6;
254255
border-radius: 4px;
@@ -257,7 +258,7 @@ main {
257258
overflow-x: auto;
258259
}
259260

260-
.profile-json pre {
261+
.json-content pre {
261262
margin: 0;
262263
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
263264
font-size: 0.9rem;
@@ -322,7 +323,7 @@ main {
322323
background-color: white;
323324
border: 1px solid #dee2e6;
324325
border-radius: 4px;
325-
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
326+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
326327
min-width: 200px;
327328
z-index: 1000;
328329
display: none;
@@ -411,7 +412,7 @@ main {
411412
background-color: white;
412413
border: 1px solid #dee2e6;
413414
border-radius: 6px;
414-
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
415+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
415416
min-width: 280px;
416417
z-index: 1000;
417418
display: none;
@@ -520,8 +521,13 @@ main {
520521
}
521522

522523
@keyframes spin {
523-
0% { transform: rotate(0deg); }
524-
100% { transform: rotate(360deg); }
524+
0% {
525+
transform: rotate(0deg);
526+
}
527+
528+
100% {
529+
transform: rotate(360deg);
530+
}
525531
}
526532

527533
/* Custom Version Modal Styles */
@@ -797,20 +803,20 @@ main {
797803
.custom-version-form {
798804
padding: 1rem;
799805
}
800-
806+
801807
.version-tags {
802808
gap: 0.25rem;
803809
}
804-
810+
805811
.version-tag {
806812
font-size: 0.8rem;
807813
padding: 0.25rem 0.5rem;
808814
}
809-
815+
810816
.form-actions {
811817
flex-direction: column-reverse;
812818
}
813-
819+
814820
.form-actions .btn {
815821
width: 100%;
816822
}
@@ -822,16 +828,16 @@ main {
822828
width: 95%;
823829
max-height: 90%;
824830
}
825-
831+
826832
.modal-header,
827833
.modal-body {
828834
padding: 1rem;
829835
}
830-
836+
831837
.account-item {
832838
padding: 0.75rem;
833839
}
834-
840+
835841
.account-avatar {
836842
width: 35px;
837843
height: 35px;

samples/msal-browser-samples/ExpressSample/public/js/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ function setupEventListeners() {
4444
e.stopPropagation();
4545
toggleDropdown(signInButton.parentElement);
4646
});
47+
48+
signInButton.style.display = '';
4749
}
4850

4951
// Toggle account dropdown

samples/msal-browser-samples/ExpressSample/public/js/auth.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export async function initializeMsal() {
3434
// Handle authentication for protected routes
3535
export async function handleProtectedRouteAuth(path) {
3636
console.log(`Attempting authentication for protected route: ${path}`);
37-
37+
3838
// First attempt SSO silent
3939
return msalInstance.ssoSilent({
4040
scopes: loginRequest.scopes
@@ -83,7 +83,7 @@ export async function signOutPopup() {
8383
const logoutRequest = {
8484
account: msalInstance.getActiveAccount()
8585
};
86-
86+
8787
await msalInstance.logoutPopup(logoutRequest);
8888
updateUI(null);
8989
showSuccess('Successfully signed out!');
@@ -99,7 +99,7 @@ export async function signOutRedirect() {
9999
const logoutRequest = {
100100
account: msalInstance.getActiveAccount()
101101
};
102-
102+
103103
await msalInstance.logoutRedirect(logoutRequest);
104104
} catch (error) {
105105
console.error('Redirect sign out failed:', error);
@@ -112,10 +112,10 @@ export async function getAccessToken() {
112112
return msalInstance.acquireTokenSilent({
113113
...loginRequest
114114
}).then((response) => {
115-
return response.accessToken;
115+
return response;
116116
}).catch(async (error) => {
117117
console.error('Silent token acquisition failed:', error);
118-
118+
119119
if (error instanceof msal.InteractionRequiredAuthError) {
120120
// Fallback to redirect
121121
await msalInstance.acquireTokenRedirect({

samples/msal-browser-samples/ExpressSample/public/js/graph.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import { getAccessToken } from './auth.js';
88
import { graphConfig } from "./authConfig.js";
99

1010
// MS Graph API call functionality
11-
export async function callMsGraph(accessToken) {
12-
if (!accessToken) {
13-
accessToken = await getAccessToken();
14-
}
11+
export async function callMsGraph() {
12+
const authResponse = await getAccessToken();
1513

1614
const headers = new Headers();
17-
const bearer = `Bearer ${accessToken}`;
15+
const bearer = `Bearer ${authResponse.accessToken}`;
1816

1917
headers.append("Authorization", bearer);
2018

@@ -28,7 +26,8 @@ export async function callMsGraph(accessToken) {
2826
if (!response.ok) {
2927
throw new Error(`HTTP error! status: ${response.status}`);
3028
}
31-
return await response.json();
29+
const profileData = await response.json();
30+
return { profileData, authResponse };
3231
} catch (error) {
3332
console.error('Error calling MS Graph:', error);
3433
throw error;
@@ -46,14 +45,15 @@ export async function loadProfileData() {
4645
if (contentElement) contentElement.style.display = 'none';
4746
if (errorElement) errorElement.style.display = 'none';
4847

49-
const profileData = await callMsGraph();
48+
const { profileData, authResponse } = await callMsGraph();
5049
displayProfileData(profileData);
50+
displayAuthData(authResponse);
5151

5252
if (loadingElement) loadingElement.style.display = 'none';
5353
if (contentElement) contentElement.style.display = 'block';
5454
} catch (error) {
5555
console.error('Failed to load profile data:', error);
56-
56+
5757
if (loadingElement) loadingElement.style.display = 'none';
5858
if (errorElement) {
5959
errorElement.textContent = 'Failed to load profile data: ' + error.message;
@@ -91,3 +91,27 @@ export function displayProfileData(profileData) {
9191
jsonElement.textContent = JSON.stringify(profileData, null, 2);
9292
}
9393
}
94+
95+
// Display authentication data in the UI
96+
export function displayAuthData(authResponse) {
97+
const authJsonElement = document.getElementById('auth-json');
98+
if (authJsonElement && authResponse) {
99+
// Create a sanitized version of the auth response for display
100+
const displayData = {
101+
accessToken: authResponse.accessToken ? '[REDACTED - Present]' : 'Not available',
102+
tokenType: authResponse.tokenType,
103+
expiresOn: authResponse.expiresOn,
104+
account: authResponse.account,
105+
scopes: authResponse.scopes,
106+
correlationId: authResponse.correlationId,
107+
fromCache: authResponse.fromCache,
108+
idToken: authResponse.idToken,
109+
idTokenClaims: authResponse.idTokenClaims,
110+
uniqueId: authResponse.uniqueId,
111+
tenantId: authResponse.tenantId
112+
};
113+
114+
authJsonElement.textContent = JSON.stringify(displayData, null, 2);
115+
authJsonElement.style.display = '';
116+
}
117+
}

0 commit comments

Comments
 (0)