Skip to content

Commit d60059b

Browse files
authored
Merge pull request #3419 from AzureAD/angular-e2e-tests
Msal-Angular E2E Tests
2 parents cf3de10 + 229d556 commit d60059b

17 files changed

+505
-64
lines changed

samples/e2eTestUtils/LabClient.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ClientSecretCredential, AccessToken } from "@azure/identity";
22
import axios from "axios";
33
import { ENV_VARIABLES, LAB_SCOPE, LAB_API_ENDPOINT, ParamKeys } from "./Constants";
44
import { LabApiQueryParams } from "./LabApiQueryParams";
5-
import dotenv from "dotenv";
5+
import * as dotenv from "dotenv";
66

77
dotenv.config({
88
path: "../../.env"

samples/e2eTestUtils/TestUtils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import fs from "fs";
2-
import puppeteer from "puppeteer";
1+
import * as fs from "fs";
2+
import * as puppeteer from "puppeteer";
33
import { LabConfig } from "./LabConfig";
44
import { LabClient } from "./LabClient";
55

samples/msal-angular-v2-samples/angular11-sample-app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@types/jasmine": "~3.6.0",
3838
"@types/node": "^12.11.1",
3939
"codelyzer": "^6.0.0",
40+
"dotenv": "^8.2.0",
4041
"jasmine-core": "~3.6.0",
4142
"jasmine-spec-reporter": "~5.0.0",
4243
"jest": "^26.6.3",

samples/msal-angular-v2-samples/angular11-sample-app/src/app/app.component.html

+12-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,18 @@
66
<a mat-button [routerLink]="['profile']">Profile</a>
77
<a mat-button [routerLink]="['lazyLoad']">Lazy-load</a>
88

9-
<button mat-raised-button *ngIf="!loginDisplay" (click)="login()">Login</button>
10-
<button mat-raised-button *ngIf="loginDisplay" (click)="logout()">Logout</button>
9+
<button mat-raised-button [matMenuTriggerFor]="loginMenu" *ngIf="!loginDisplay">Login</button>
10+
<mat-menu #loginMenu="matMenu">
11+
<button mat-menu-item (click)="loginRedirect()">Login using Redirect</button>
12+
<button mat-menu-item (click)="loginPopup()">Login using Popup</button>
13+
</mat-menu>
14+
15+
<button mat-raised-button [matMenuTriggerFor]="logoutMenu" *ngIf="loginDisplay">Logout</button>
16+
<mat-menu #logoutMenu="matMenu">
17+
<button mat-menu-item (click)="logout()">Logout using Redirect</button>
18+
<button mat-menu-item (click)="logout(true)">Logout using Popup</button>
19+
</mat-menu>
20+
1121
</mat-toolbar>
1222
<div class="container">
1323
<!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->

samples/msal-angular-v2-samples/angular11-sample-app/src/app/app.component.ts

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
22
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
3-
import { AuthenticationResult, InteractionStatus, InteractionType, PopupRequest, RedirectRequest } from '@azure/msal-browser';
3+
import { AuthenticationResult, InteractionStatus, PopupRequest, RedirectRequest } from '@azure/msal-browser';
44
import { Subject } from 'rxjs';
55
import { filter, takeUntil } from 'rxjs/operators';
66

@@ -53,30 +53,30 @@ export class AppComponent implements OnInit, OnDestroy {
5353
}
5454
}
5555

56-
login() {
57-
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
58-
if (this.msalGuardConfig.authRequest){
59-
this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
60-
.subscribe((response: AuthenticationResult) => {
61-
this.authService.instance.setActiveAccount(response.account);
62-
});
63-
} else {
64-
this.authService.loginPopup()
65-
.subscribe((response: AuthenticationResult) => {
66-
this.authService.instance.setActiveAccount(response.account);
67-
});
68-
}
56+
loginRedirect() {
57+
if (this.msalGuardConfig.authRequest){
58+
this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
6959
} else {
70-
if (this.msalGuardConfig.authRequest){
71-
this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
60+
this.authService.loginRedirect();
61+
}
62+
}
63+
64+
loginPopup() {
65+
if (this.msalGuardConfig.authRequest){
66+
this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
67+
.subscribe((response: AuthenticationResult) => {
68+
this.authService.instance.setActiveAccount(response.account);
69+
});
7270
} else {
73-
this.authService.loginRedirect();
74-
}
71+
this.authService.loginPopup()
72+
.subscribe((response: AuthenticationResult) => {
73+
this.authService.instance.setActiveAccount(response.account);
74+
});
7575
}
7676
}
7777

78-
logout() {
79-
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
78+
logout(popup?: boolean) {
79+
if (popup) {
8080
this.authService.logoutPopup({
8181
mainWindowRedirectUri: "/"
8282
});

samples/msal-angular-v2-samples/angular11-sample-app/src/app/app.module.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NgModule } from '@angular/core';
55
import { MatButtonModule } from '@angular/material/button';
66
import { MatToolbarModule } from '@angular/material/toolbar';
77
import { MatListModule } from '@angular/material/list';
8+
import { MatMenuModule } from '@angular/material/menu';
89

910
import { AppRoutingModule } from './app-routing.module';
1011
import { AppComponent } from './app.component';
@@ -21,10 +22,14 @@ const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigato
2122
export function loggerCallback(logLevel: LogLevel, message: string) {
2223
console.log(message);
2324
}
25+
2426
export function MSALInstanceFactory(): IPublicClientApplication {
2527
return new PublicClientApplication({
2628
auth: {
27-
clientId: '6226576d-37e9-49eb-b201-ec1eeb0029b6',
29+
// clientId: '6226576d-37e9-49eb-b201-ec1eeb0029b6', // Prod enviroment. Uncomment to use.
30+
clientId: '3fba556e-5d4a-48e3-8e1a-fd57c12cb82e', // PPE testing environment
31+
// authority: "https://login.windows-ppe.net/common", // Prod environment. Uncomment to use.
32+
authority: 'https://login.windows-ppe.net/common', // PPE testing environment.
2833
redirectUri: '/',
2934
postLogoutRedirectUri: '/'
3035
},
@@ -44,7 +49,8 @@ export function MSALInstanceFactory(): IPublicClientApplication {
4449

4550
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
4651
const protectedResourceMap = new Map<string, Array<string>>();
47-
protectedResourceMap.set('https://graph.microsoft.com/v1.0/me', ['user.read']);
52+
// protectedResourceMap.set('https://graph.microsoft.com/v1.0/me', ['user.read']); // Prod environment. Uncomment to use.
53+
protectedResourceMap.set('https://graph.microsoft-ppe.com/v1.0/me', ['user.read']);
4854

4955
return {
5056
interactionType: InteractionType.Redirect,
@@ -76,6 +82,7 @@ export function MSALGuardConfigFactory(): MsalGuardConfiguration {
7682
MatButtonModule,
7783
MatToolbarModule,
7884
MatListModule,
85+
MatMenuModule,
7986
HttpClientModule,
8087
MsalModule
8188
],

samples/msal-angular-v2-samples/angular11-sample-app/src/app/detail/detail.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Component, OnInit } from '@angular/core';
22
import { HttpClient } from '@angular/common/http';
33

4-
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';
4+
// const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me'; // Prod graph endpoint. Uncomment to use.
5+
const GRAPH_ENDPOINT = 'https://graph.microsoft-ppe.com/v1.0/me';
56

67
type DetailType = {
78
displayName?: string

samples/msal-angular-v2-samples/angular11-sample-app/src/app/home/home.component.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnInit } from '@angular/core';
22
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
3-
import { AuthenticationResult, EventMessage, EventType } from '@azure/msal-browser';
3+
import { AuthenticationResult, EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
44
import { filter } from 'rxjs/operators';
55

66
@Component({
@@ -24,7 +24,13 @@ export class HomeComponent implements OnInit {
2424
this.authService.instance.setActiveAccount(payload.account);
2525
});
2626

27-
this.setLoginDisplay();
27+
this.msalBroadcastService.inProgress$
28+
.pipe(
29+
filter((status: InteractionStatus) => status === InteractionStatus.None)
30+
)
31+
.subscribe(() => {
32+
this.setLoginDisplay();
33+
})
2834

2935
}
3036

samples/msal-angular-v2-samples/angular11-sample-app/src/app/profile/profile.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Component, OnInit } from '@angular/core';
22
import { HttpClient } from '@angular/common/http';
33

4-
const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';
4+
// const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me'; // Prod graph endpoint. Uncomment to use.
5+
const GRAPH_ENDPOINT = 'https://graph.microsoft-ppe.com/v1.0/me';
56

67
type ProfileType = {
78
givenName?: string,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as puppeteer from "puppeteer";
2+
import { Screenshot, setupCredentials, enterCredentials } from "../../../e2eTestUtils/TestUtils";
3+
import { LabClient } from "../../../e2eTestUtils/LabClient";
4+
import { LabApiQueryParams } from "../../../e2eTestUtils/LabApiQueryParams";
5+
import { AzureEnvironments, AppTypes } from "../../../e2eTestUtils/Constants";
6+
import { BrowserCacheUtils } from "../../../e2eTestUtils/BrowserCacheTestUtils";
7+
8+
const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/detail-tests`;
9+
10+
async function verifyTokenStore(BrowserCache: BrowserCacheUtils, scopes: string[]): Promise<void> {
11+
const tokenStore = await BrowserCache.getTokens();
12+
expect(tokenStore.idTokens.length).toBe(1);
13+
expect(tokenStore.accessTokens.length).toBe(1);
14+
expect(tokenStore.refreshTokens.length).toBe(1);
15+
expect(await BrowserCache.getAccountFromCache(tokenStore.idTokens[0])).not.toBeNull();
16+
expect(await BrowserCache.accessTokenForScopesExists(tokenStore.accessTokens, scopes)).toBeTruthy;
17+
const storage = await BrowserCache.getWindowStorage();
18+
expect(Object.keys(storage).length).toBe(5);
19+
}
20+
21+
describe('/ (Detail Page)', () => {
22+
let browser: puppeteer.Browser;
23+
let context: puppeteer.BrowserContext;
24+
let page: puppeteer.Page;
25+
let port: number;
26+
let username: string;
27+
let accountPwd: string;
28+
let BrowserCache: BrowserCacheUtils;
29+
30+
beforeAll(async () => {
31+
// @ts-ignore
32+
browser = await global.__BROWSER__;
33+
// @ts-ignore
34+
port = global.__PORT__;
35+
36+
const labApiParams: LabApiQueryParams = {
37+
azureEnvironment: AzureEnvironments.PPE,
38+
appType: AppTypes.CLOUD
39+
};
40+
41+
const labClient = new LabClient();
42+
const envResponse = await labClient.getVarsByCloudEnvironment(labApiParams);
43+
44+
[username, accountPwd] = await setupCredentials(envResponse[0], labClient);
45+
});
46+
47+
beforeEach(async () => {
48+
context = await browser.createIncognitoBrowserContext();
49+
page = await context.newPage();
50+
BrowserCache = new BrowserCacheUtils(page, "localStorage");
51+
await page.goto(`http://localhost:${port}`);
52+
});
53+
54+
afterEach(async () => {
55+
await page.close();
56+
await context.close();
57+
});
58+
59+
it("Detail page - children are rendered after clicking profile, logging in with redirect, and detail buttons clicked", async () => {
60+
const testName = "detailsBaseCase";
61+
const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
62+
await screenshot.takeScreenshot(page, "Page loaded");
63+
64+
// Initiate Login via MsalGuard by clicking Profile
65+
const [profileButton] = await page.$x("//span[contains(., 'Profile')]");
66+
await profileButton.click();
67+
68+
await enterCredentials(page, screenshot, username, accountPwd);
69+
70+
// Verify UI now displays logged in content
71+
const [logoutButtons] = await page.$x("//button[contains(., 'Logout')]");
72+
expect(logoutButtons).toBeDefined();
73+
await screenshot.takeScreenshot(page, "Profile page signed in");
74+
75+
// Verify tokens are in cache
76+
await verifyTokenStore(BrowserCache, ["User.Read"]);
77+
78+
// Verify displays profile page
79+
const [profileFirstName] = await page.$x("//strong[contains(., 'First Name: ')]");
80+
expect(profileFirstName).toBeDefined();
81+
82+
// Navigate to details page
83+
const [detailsButton] = await page.$x("//a[contains(., 'details')]");
84+
await detailsButton.click();
85+
await screenshot.takeScreenshot(page, "Details page");
86+
87+
// Wait for Graph data to display
88+
await page.waitForXPath("//p[contains(., 'Details for: ')]", {timeout: 5000});
89+
await screenshot.takeScreenshot(page, "Graph data acquired");
90+
91+
// Verify displays details page
92+
const [detailsFor] = await page.$x("//p[contains(., 'Details for: ')]");
93+
expect(detailsFor).toBeDefined();
94+
});
95+
}
96+
);

0 commit comments

Comments
 (0)