-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Identity] Two Playwright tests for the InteractiveBrowserCredential #21171
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,7 +52,8 @@ | |
"check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", | ||
"integration-test:browser": "echo skipped", | ||
"integration-test:node": "dev-tool run test:node-js-input -- --timeout 180000 'dist-esm/test/public/node/*.spec.js' 'dist-esm/test/internal/node/*.spec.js'", | ||
"integration-test": "npm run integration-test:node && npm run integration-test:browser", | ||
"integration-test:playwright": "rimraf dist-playwright && npx playwright install && tsc -p playwright.tsconfig.json && rollup --config && playwright test -c dist-playwright", | ||
"integration-test": "npm run integration-test:node && npm run integration-test:browser && npm run integration-test:playwright", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m having a hard time imagining how to merge this with our current setup 😅 |
||
"lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", | ||
"lint": "eslint package.json api-extractor.json src test --ext .ts", | ||
"pack": "npm pack 2>&1", | ||
|
@@ -126,16 +127,23 @@ | |
"@azure/test-utils": "^1.0.0", | ||
"@azure-tools/test-recorder": "^2.0.0", | ||
"@microsoft/api-extractor": "^7.18.11", | ||
"@playwright/test": "^1.19.2", | ||
"@rollup/plugin-commonjs": "^21.0.2", | ||
"@rollup/plugin-json": "^4.1.0", | ||
"@rollup/plugin-node-resolve": "^13.1.3", | ||
"@rollup/plugin-typescript": "^8.3.1", | ||
"@types/jws": "^3.2.2", | ||
"@types/mocha": "^7.0.2", | ||
"@types/node": "^12.0.0", | ||
"@types/uuid": "^8.0.0", | ||
"@types/chai": "^4.1.6", | ||
"@types/express": "^4.17.13", | ||
"@types/stoppable": "^1.1.0", | ||
"chai": "^4.2.0", | ||
"cross-env": "^7.0.2", | ||
"dotenv": "^8.2.0", | ||
"eslint": "^7.15.0", | ||
"express": "^4.17.3", | ||
"inherits": "^2.0.3", | ||
"karma": "^6.2.0", | ||
"karma-chrome-launcher": "^3.0.0", | ||
|
@@ -151,6 +159,8 @@ | |
"prettier": "^2.5.1", | ||
"puppeteer": "^13.5.1", | ||
"rimraf": "^3.0.0", | ||
"rollup": "^2.70.1", | ||
"rollup-plugin-shim": "^1.0.0", | ||
"typescript": "~4.2.0", | ||
"util": "^0.12.1", | ||
"sinon": "^9.0.2", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ESNext", | ||
"module": "commonjs", | ||
"moduleResolution": "Node", | ||
"sourceMap": true, | ||
"outDir": "./dist-playwright" | ||
}, | ||
"include": ["test/playwright"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import resolve from "@rollup/plugin-node-resolve"; | ||
import cjs from "@rollup/plugin-commonjs"; | ||
import json from "@rollup/plugin-json"; | ||
import typescript from "@rollup/plugin-typescript"; | ||
import shim from "rollup-plugin-shim"; | ||
|
||
export default { | ||
input: "./test/playwright/rollup/src/index.ts", | ||
output: { | ||
file: "test/playwright/rollup/dist/index.js", | ||
format: "umd", | ||
name: "main", | ||
}, | ||
plugins: [ | ||
shim({}), | ||
resolve({ | ||
preferBuiltins: false, | ||
mainFields: ["module", "browser"], | ||
}), | ||
cjs(), | ||
json(), | ||
typescript(), | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,10 @@ | ||||||||||
<!DOCTYPE html> | ||||||||||
<html> | ||||||||||
<head> | ||||||||||
<meta charset="utf-8" /> | ||||||||||
<!-- /index.js is a route defined in the Express.js server --> | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Suggested change
|
||||||||||
<script src="/index.js"></script> | ||||||||||
</head> | ||||||||||
<body> | ||||||||||
</body> | ||||||||||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { Page } from "@playwright/test"; | ||
|
||
export async function preparePage(page: Page): Promise<void> { | ||
// Log and continue all network requests | ||
await page.route("**", (route) => { | ||
console.log("PLAYWRIGHT PAGE ROUTE:", route.request().url()); | ||
route.continue(); | ||
}); | ||
|
||
// Logging the page's console.logs | ||
page.on("console", async (msg) => { | ||
const values = []; | ||
for (const arg of msg.args()) { | ||
values.push(await arg.jsonValue()); | ||
} | ||
console.log(...values); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import * as dotenv from "dotenv"; | ||
import { test, expect } from "@playwright/test"; | ||
import { isLiveMode } from "@azure-tools/test-recorder"; | ||
import { prepareServer } from "./server"; | ||
import { preparePage } from "./page"; | ||
|
||
dotenv.config(); | ||
|
||
const clientId = process.env.AZURE_CLIENT_ID || process.env.AZURE_IDENTITY_BROWSER_CLIENT_ID; | ||
const azureUsername = process.env.AZURE_USERNAME || process.env.AZURE_IDENTITY_TEST_USERNAME; | ||
const azurePassword = process.env.AZURE_PASSWORD || process.env.AZURE_IDENTITY_TEST_PASSWORD; | ||
const port = process.env.PORT || "8080"; | ||
const scope = "https://graph.microsoft.com/.default"; | ||
|
||
// The Azure Active Directory app registration should be of the type | ||
// "spa" and the redirect endpoint should point to: | ||
const homeUri = `http://localhost:${port}/`; | ||
|
||
const credentialOptions = { redirectUri: homeUri }; | ||
|
||
test("Authenticates with a popup", async ({ page }) => { | ||
test.skip(!isLiveMode(), "Playwright tests can only run on live mode"); | ||
test.skip(!clientId, "Client ID environment variable required"); | ||
test.skip(!azureUsername, "Username environment variable required"); | ||
test.skip(!azurePassword, "Password environment variable required"); | ||
|
||
const { start, stop } = await prepareServer({ port }); | ||
await preparePage(page); | ||
await start(); | ||
|
||
// THE TEST BEGINS | ||
|
||
// We go to the home page | ||
await page.goto(homeUri); | ||
|
||
const [popup] = await Promise.all([ | ||
page.waitForEvent("popup"), | ||
page.evaluate( | ||
async ({ clientId, scope, credentialOptions }) => { | ||
const { InteractiveBrowserCredential } = (window as any).main; | ||
|
||
const credential = new InteractiveBrowserCredential({ | ||
...credentialOptions, | ||
clientId, | ||
}); | ||
|
||
// The redirection to Azure happens here... | ||
credential.getToken(scope); | ||
}, | ||
{ | ||
clientId, | ||
scope, | ||
credentialOptions, | ||
} | ||
), | ||
]); | ||
|
||
// Interactive popup login with Playwright | ||
await popup.waitForNavigation(); | ||
await popup.waitForSelector(`input[type="email"]`); | ||
await popup.fill(`input[type="email"]`, azureUsername!); | ||
await popup.waitForSelector(`input[type="submit"]`); | ||
await popup.click(`input[type="submit"]`); | ||
await popup.waitForLoadState("networkidle"); | ||
await popup.waitForSelector(`input[type="password"]`); | ||
await popup.fill(`input[type="password"]`, azurePassword!); | ||
await popup.waitForSelector(`input[type="submit"]`); | ||
await popup.click(`input[type="submit"]`); | ||
await popup.waitForSelector(`input[type="submit"]`); | ||
await popup.click(`input[type="submit"]`); | ||
await popup.waitForEvent("close"); | ||
|
||
const token = await page.evaluate( | ||
async ({ clientId, scope, credentialOptions }) => { | ||
const { InteractiveBrowserCredential } = (window as any).main; | ||
|
||
const credential = new InteractiveBrowserCredential({ | ||
clientId, | ||
...credentialOptions, | ||
}); | ||
|
||
return await credential.getToken(scope); | ||
}, | ||
{ clientId, scope, credentialOptions } | ||
); | ||
|
||
expect(token).toBeTruthy(); | ||
expect(token.token).toBeTruthy(); | ||
expect(token.expiresOnTimestamp).toBeTruthy(); | ||
|
||
await stop(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import * as dotenv from "dotenv"; | ||
import { test, expect } from "@playwright/test"; | ||
import { isLiveMode } from "@azure-tools/test-recorder"; | ||
import { prepareServer } from "./server"; | ||
import { preparePage } from "./page"; | ||
|
||
dotenv.config(); | ||
|
||
const clientId = process.env.AZURE_CLIENT_ID || process.env.AZURE_IDENTITY_BROWSER_CLIENT_ID; | ||
const azureUsername = process.env.AZURE_USERNAME || process.env.AZURE_IDENTITY_TEST_USERNAME; | ||
const azurePassword = process.env.AZURE_PASSWORD || process.env.AZURE_IDENTITY_TEST_PASSWORD; | ||
const port = process.env.PORT || "8080"; | ||
const scope = "https://graph.microsoft.com/.default"; | ||
|
||
// The Azure Active Directory app registration should be of the type | ||
// "spa" and the redirect endpoint should point to: | ||
const homeUri = `http://localhost:${port}/`; | ||
|
||
const credentialOptions = { redirectUri: homeUri }; | ||
|
||
test("Authenticates", async ({ page }) => { | ||
test.skip(!isLiveMode(), "Playwright tests can only run on live mode"); | ||
test.skip(!clientId, "Client ID environment variable required"); | ||
test.skip(!azureUsername, "Username environment variable required"); | ||
test.skip(!azurePassword, "Password environment variable required"); | ||
|
||
const { start, stop } = await prepareServer({ port }); | ||
await preparePage(page); | ||
await start(); | ||
|
||
// THE TEST BEGINS | ||
|
||
// We go to the home page | ||
await page.goto(homeUri); | ||
|
||
await page.evaluate( | ||
async ({ clientId, scope, credentialOptions }) => { | ||
const { InteractiveBrowserCredential } = (window as any).main; | ||
|
||
const credential = new InteractiveBrowserCredential({ | ||
...credentialOptions, | ||
clientId, | ||
loginStyle: "redirect", | ||
}); | ||
|
||
// The redirection to Azure happens here... | ||
credential.getToken(scope); | ||
}, | ||
{ clientId, scope, credentialOptions } | ||
); | ||
|
||
// Interactive login with Playwright | ||
await page.waitForNavigation(); | ||
await page.waitForSelector(`input[type="email"]`); | ||
await page.fill(`input[type="email"]`, azureUsername!); | ||
await page.waitForSelector(`input[type="submit"]`); | ||
await page.click(`input[type="submit"]`); | ||
await page.waitForLoadState("networkidle"); | ||
await page.waitForSelector(`input[type="password"]`); | ||
await page.fill(`input[type="password"]`, azurePassword!); | ||
await page.waitForSelector(`input[type="submit"]`); | ||
await page.click(`input[type="submit"]`); | ||
await page.waitForSelector(`input[type="submit"]`); | ||
await page.click(`input[type="submit"]`); | ||
await page.waitForURL(`${homeUri}**`); | ||
|
||
const token = await page.evaluate( | ||
async ({ clientId, scope, credentialOptions }) => { | ||
const { InteractiveBrowserCredential } = (window as any).main; | ||
|
||
const credential = new InteractiveBrowserCredential({ | ||
...credentialOptions, | ||
clientId, | ||
loginStyle: "redirect", | ||
}); | ||
|
||
return await credential.getToken(scope); | ||
}, | ||
{ clientId, scope, credentialOptions } | ||
); | ||
|
||
expect(token).toBeTruthy(); | ||
expect(token.token).toBeTruthy(); | ||
expect(token.expiresOnTimestamp).toBeTruthy(); | ||
|
||
await stop(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
export { InteractiveBrowserCredential } from "../../../../src/credentials/interactiveBrowserCredential.browser"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I were to do this in another project, other than our SDK repo, I would load here the export { InteractiveBrowserCredential } from "@azure/identity"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import * as express from "express"; | ||
import { readFileSync } from "fs"; | ||
import { Server } from "http"; | ||
|
||
// A simple web server that allows passing configuration and behavioral parameters. | ||
// This file should be thought as the archetypicall representation of a web server, | ||
// whereas the test file will include the nuance specific to testing the desired behavior. | ||
|
||
/** | ||
* Options to the server. | ||
* With the intent to make the server parametrizable! | ||
*/ | ||
export interface ServerOptions { | ||
/** | ||
* Port number as a string | ||
*/ | ||
port: string; | ||
} | ||
|
||
/** | ||
* Result of the prepareServer function. | ||
*/ | ||
export interface PepareServerResult { | ||
app: express.Application; | ||
start: () => Promise<void>; | ||
stop: () => Promise<void>; | ||
} | ||
|
||
/** | ||
* Sets up a parametrizable Express server. | ||
*/ | ||
export async function prepareServer(serverOptions: ServerOptions): Promise<PepareServerResult> { | ||
const app = express(); | ||
|
||
/** | ||
* Logging calls. | ||
*/ | ||
app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => { | ||
console.log("Playwright Express test server:", req.url); | ||
next(); | ||
}); | ||
|
||
/** | ||
* Endpoint that loads the index.js | ||
*/ | ||
app.get("/index.js", async (_req: express.Request, res: express.Response) => { | ||
const indexContent = readFileSync("./test/playwright/rollup/dist/index.js", { | ||
encoding: "utf8", | ||
}); | ||
res.send(indexContent); | ||
}); | ||
|
||
/** | ||
* Home URI | ||
*/ | ||
app.get("/", async (_req: express.Request, res: express.Response) => { | ||
const indexContent = readFileSync("./test/playwright/index.html", { encoding: "utf8" }); | ||
res.send(indexContent); | ||
}); | ||
|
||
let server: Server | undefined = undefined; | ||
|
||
return { | ||
app, | ||
async start() { | ||
server = app.listen(serverOptions.port, () => { | ||
console.log(`Authorization code redirect server listening on port ${serverOptions.port}`); | ||
}); | ||
}, | ||
async stop() { | ||
if (server) { | ||
server.close(); | ||
} | ||
}, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,3 +33,4 @@ stages: | |
IDENTITY_SP_CERT_SNI: $(Agent.TempDirectory)/testsni.pfx | ||
IDENTITY_SP_CERT_SNI_PEM: $(Agent.TempDirectory)/testsni.pem | ||
IDENTITY_PEM_CONTENTS: $(net-identity-spcert-pem) | ||
AZURE_IDENTITY_BROWSER_CLIENT_ID: $(identity-azure-sdk-js-test-browser-client-id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to create an app registration with an “spa” redirect endpoint and store its client ID in the keyvault used for the CI en variables 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
npx playwright install
ensures that the web browser needed for testing is installed. Playwright doesn’t do that by default when you install it.