Skip to content

Commit 1c83dec

Browse files
authored
Merge pull request #3425 from AzureAD/angular10-add-e2e
Msal-Angular E2E Tests #2
2 parents d60059b + 65cb2d4 commit 1c83dec

File tree

21 files changed

+507
-129
lines changed

21 files changed

+507
-129
lines changed

samples/e2eTestUtils/BrowserCacheTestUtils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import puppeteer from "puppeteer";
1+
import * as puppeteer from "puppeteer";
22

33
import { LabConfig } from "./LabConfig";
44
import { Configuration } from "../../lib/msal-browser";

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"@angular/platform-browser": "~10.0.9",
2424
"@angular/platform-browser-dynamic": "~10.0.9",
2525
"@angular/router": "~10.0.9",
26-
"@azure/msal-angular": "^2.0.0-beta.2",
27-
"@azure/msal-browser": "^2.13.0",
26+
"@azure/msal-angular": "^2.0.0-beta.4",
27+
"@azure/msal-browser": "^2.14.0",
2828
"rxjs": "~6.5.5",
2929
"tslib": "^2.0.0",
3030
"zone.js": "~0.10.3"
@@ -37,6 +37,7 @@
3737
"@types/jasminewd2": "~2.0.3",
3838
"@types/node": "^12.11.1",
3939
"codelyzer": "^6.0.0",
40+
"dotenv": "^8.2.0",
4041
"jasmine-core": "~3.5.0",
4142
"jasmine-spec-reporter": "~5.0.0",
4243
"jest": "^26.6.3",

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55

66
<a mat-button [routerLink]="['profile']">Profile</a>
77

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

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

+20-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
22
import { Router } from '@angular/router';
33
import { Location } from "@angular/common";
44
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration, MsalCustomNavigationClient } from '@azure/msal-angular';
5-
import { AuthenticationResult, InteractionType, InteractionStatus, PopupRequest, RedirectRequest } from '@azure/msal-browser';
5+
import { AuthenticationResult, InteractionStatus, PopupRequest, RedirectRequest } from '@azure/msal-browser';
66
import { Subject } from 'rxjs';
77
import { filter, takeUntil } from 'rxjs/operators';
88

@@ -46,30 +46,30 @@ export class AppComponent implements OnInit, OnDestroy {
4646
this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
4747
}
4848

49-
login() {
50-
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
51-
if (this.msalGuardConfig.authRequest){
52-
this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
53-
.subscribe((response: AuthenticationResult) => {
54-
this.authService.instance.setActiveAccount(response.account);
55-
});
56-
} else {
57-
this.authService.loginPopup()
58-
.subscribe((response: AuthenticationResult) => {
59-
this.authService.instance.setActiveAccount(response.account);
60-
});
61-
}
49+
loginRedirect() {
50+
if (this.msalGuardConfig.authRequest){
51+
this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
6252
} else {
63-
if (this.msalGuardConfig.authRequest){
64-
this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
53+
this.authService.loginRedirect();
54+
}
55+
}
56+
57+
loginPopup() {
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+
});
6563
} else {
66-
this.authService.loginRedirect();
67-
}
64+
this.authService.loginPopup()
65+
.subscribe((response: AuthenticationResult) => {
66+
this.authService.instance.setActiveAccount(response.account);
67+
});
6868
}
6969
}
7070

71-
logout() {
72-
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
71+
logout(popup?: boolean) {
72+
if (popup) {
7373
this.authService.logoutPopup({
7474
mainWindowRedirectUri: "/"
7575
});

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

+8-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';
@@ -24,7 +25,10 @@ export function loggerCallback(logLevel: LogLevel, message: string) {
2425
export function MSALInstanceFactory(): IPublicClientApplication {
2526
return new PublicClientApplication({
2627
auth: {
27-
clientId: '6226576d-37e9-49eb-b201-ec1eeb0029b6',
28+
// clientId: '6226576d-37e9-49eb-b201-ec1eeb0029b6', // Prod enviroment. Uncomment to use.
29+
clientId: '3fba556e-5d4a-48e3-8e1a-fd57c12cb82e', // PPE testing environment
30+
// authority: 'https://login.microsoftonline.com/common', // Prod environment. Uncomment to use.
31+
authority: 'https://login.windows-ppe.net/common', // PPE testing environment.
2832
redirectUri: '/',
2933
postLogoutRedirectUri: '/'
3034
},
@@ -44,7 +48,8 @@ export function MSALInstanceFactory(): IPublicClientApplication {
4448

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

4954
return {
5055
interactionType: InteractionType.Redirect,
@@ -74,6 +79,7 @@ export function MSALGuardConfigFactory(): MsalGuardConfiguration {
7479
MatButtonModule,
7580
MatToolbarModule,
7681
MatListModule,
82+
MatMenuModule,
7783
HttpClientModule,
7884
MsalModule
7985
],

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

+9-3
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({
@@ -13,7 +13,7 @@ export class HomeComponent implements OnInit {
1313

1414
constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
1515

16-
ngOnInit(): void {
16+
ngOnInit(): void {
1717
this.msalBroadcastService.msalSubject$
1818
.pipe(
1919
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
@@ -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
setLoginDisplay() {

samples/msal-angular-v2-samples/angular10-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,142 @@
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/home-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(4);
19+
}
20+
21+
describe('/ (Home 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("Home page - children are rendered after logging in with loginRedirect", async () => {
60+
const testName = "redirectBaseCase";
61+
const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
62+
await screenshot.takeScreenshot(page, "Page loaded");
63+
64+
// Initiate Login
65+
const [signInButton] = await page.$x("//button[contains(., 'Login')]");
66+
await signInButton.click();
67+
await page.waitForTimeout(70);
68+
await screenshot.takeScreenshot(page, "Login button clicked");
69+
const [loginRedirectButton] = await page.$x("//div//button[contains(., 'Login using Redirect')]");
70+
await loginRedirectButton.click();
71+
72+
await enterCredentials(page, screenshot, username, accountPwd);
73+
74+
// Verify UI now displays logged in content
75+
const [signedIn] = await page.$x("//p[contains(., 'Login successful!')]");
76+
expect(signedIn).toBeDefined();
77+
const [logoutButton] = await page.$x("//button[contains(., 'Logout')]");
78+
await logoutButton.click();
79+
const logoutButtons = await page.$x("//div//button[contains(., 'Logout using')]");
80+
expect(logoutButtons.length).toBe(2);
81+
await logoutButton.click();
82+
await screenshot.takeScreenshot(page, "App signed in");
83+
84+
// Verify tokens are in cache
85+
await verifyTokenStore(BrowserCache, ["User.Read"]);
86+
87+
// Navigate to profile page
88+
const [profileButton] = await page.$x("//span[contains(., 'Profile')]");
89+
await profileButton.click();
90+
await screenshot.takeScreenshot(page, "Profile page loaded");
91+
92+
// Verify displays profile page without activating MsalGuard
93+
const [profileFirstName] = await page.$x("//strong[contains(., 'First Name: ')]");
94+
expect(profileFirstName).toBeDefined();
95+
});
96+
97+
it("Home page - children are rendered after logging in with loginPopup", async () => {
98+
const testName = "popupBaseCase";
99+
const screenshot = new Screenshot(`${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`);
100+
await screenshot.takeScreenshot(page, "Page loaded");
101+
102+
// Initiate Login
103+
const [signInButton] = await page.$x("//button[contains(., 'Login')]");
104+
await signInButton.click();
105+
await page.waitForTimeout(70);
106+
await screenshot.takeScreenshot(page, "Login button clicked");
107+
const [loginPopupButton] = await page.$x("//button[contains(., 'Login using Popup')]");
108+
const newPopupWindowPromise = new Promise<puppeteer.Page>(resolve => page.once("popup", resolve));
109+
await loginPopupButton.click();
110+
const popupPage = await newPopupWindowPromise;
111+
const popupWindowClosed = new Promise<void>(resolve => popupPage.once("close", resolve));
112+
113+
await enterCredentials(popupPage, screenshot, username, accountPwd);
114+
await popupWindowClosed;
115+
116+
await page.waitForXPath("//p[contains(., 'Login successful!')]", {timeout: 3000});
117+
await screenshot.takeScreenshot(page, "Popup closed");
118+
119+
// Verify UI now displays logged in content
120+
const [signedIn] = await page.$x("//p[contains(., 'Login successful!')]");
121+
expect(signedIn).toBeDefined();
122+
const [logoutButton] = await page.$x("//button[contains(., 'Logout')]");
123+
await logoutButton.click();
124+
const logoutButtons = await page.$x("//button[contains(., 'Logout using')]");
125+
expect(logoutButtons.length).toBe(2);
126+
await logoutButton.click();
127+
await screenshot.takeScreenshot(page, "App signed in");
128+
129+
// Verify tokens are in cache
130+
await verifyTokenStore(BrowserCache, ["User.Read"]);
131+
132+
// Navigate to profile page
133+
const [profileButton] = await page.$x("//span[contains(., 'Profile')]");
134+
await profileButton.click();
135+
await screenshot.takeScreenshot(page, "Profile page loaded");
136+
137+
// Verify displays profile page without activating MsalGuard
138+
const [profileFirstName] = await page.$x("//strong[contains(., 'First Name: ')]");
139+
expect(profileFirstName).toBeDefined();
140+
});
141+
}
142+
);

0 commit comments

Comments
 (0)