diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index b32ed17f8..36055bf96 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Micr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.MicrosoftGraphBeta", "src\Microsoft.Identity.Web.MicrosoftGraphBeta\Microsoft.Identity.Web.MicrosoftGraphBeta.csproj", "{B68226EF-2A43-4040-A583-061F25A4DE21}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAppUiTests", "tests\IntegrationTests\WebAppUiTests\WebAppUiTests.csproj", "{25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -175,6 +177,10 @@ Global {69ECC686-3077-4E46-97F6-D287A4821FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {69ECC686-3077-4E46-97F6-D287A4821FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {69ECC686-3077-4E46-97F6-D287A4821FD4}.Release|Any CPU.Build.0 = Release|Any CPU + {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Release|Any CPU.Build.0 = Release|Any CPU {9A736AA6-54DD-47E0-85D9-3CFEE2CDD92A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9A736AA6-54DD-47E0-85D9-3CFEE2CDD92A}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A736AA6-54DD-47E0-85D9-3CFEE2CDD92A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -183,10 +189,10 @@ Global {B68226EF-2A43-4040-A583-061F25A4DE21}.Debug|Any CPU.Build.0 = Debug|Any CPU {B68226EF-2A43-4040-A583-061F25A4DE21}.Release|Any CPU.ActiveCfg = Release|Any CPU {B68226EF-2A43-4040-A583-061F25A4DE21}.Release|Any CPU.Build.0 = Release|Any CPU - {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3C5B5F5-617E-4206-B9EF-915A27AF6863}.Release|Any CPU.Build.0 = Release|Any CPU + {25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -220,6 +226,7 @@ Global {69ECC686-3077-4E46-97F6-D287A4821FD4} = {E571B66E-C693-445D-9DF8-A4C2CB9B69CD} {3EF671C3-7BD5-4125-8A98-EC34549BCDDD} = {79310504-1334-4F14-93C4-1240913224BA} {D3C5B5F5-617E-4206-B9EF-915A27AF6863} = {3EF671C3-7BD5-4125-8A98-EC34549BCDDD} + {25C82DF8-F0C8-4B6C-B8BA-6018C40ADE6F} = {3EF671C3-7BD5-4125-8A98-EC34549BCDDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F4FA8C4C-3251-41CC-939B-7892F5798549} diff --git a/tests/IntegrationTests/WebAppUiTests/WebAppIntegrationTests.cs b/tests/IntegrationTests/WebAppUiTests/WebAppIntegrationTests.cs new file mode 100644 index 000000000..fcb1d166a --- /dev/null +++ b/tests/IntegrationTests/WebAppUiTests/WebAppIntegrationTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.Test.LabInfrastructure; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using Xunit; + +namespace WebAppUiTests +{ +#if !FROM_GITHUB_ACTION + public class WebAppIntegrationTests + { + [Fact] + public async Task ChallengeUser_SignInSucceedsTestAsync() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } + + // Arrange + LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false); + + ChromeOptions options = new ChromeOptions(); + // ~2x faster, no visual rendering + // comment-out below when debugging to see the UI automation + options.AddArguments(TestConstants.Headless); + IWebDriver driver = new ChromeDriver(options); + driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); + + // Act + Trace.WriteLine("Starting Selenium automation: web app sign-in & call Graph"); + driver.Navigate() + .GoToUrl("https://webapptestmsidweb.azurewebsites.net/MicrosoftIdentity/Account/signin"); + PerformLogin(driver, labResponse.User); + + // Assert + Assert.Contains(labResponse.User.Upn, driver.PageSource); + Assert.Contains(TestConstants.PhotoLabel, driver.PageSource); + driver.Quit(); + driver.Dispose(); + } + + private static void PerformLogin( + IWebDriver driver, + LabUser user) + { + UserInformationFieldIds fields = new UserInformationFieldIds(); + + EnterUsername(driver, user, fields); + EnterPassword(driver, user, fields); + HandleStaySignedInPrompt(driver); + } + + private static void EnterUsername( + IWebDriver driver, + LabUser user, + UserInformationFieldIds fields) + { + // Lab user needs to be a guest in the msidentity-samples-testing tenant + Trace.WriteLine(string.Format("Logging in ... Entering user name: {0}", user.Upn)); + + driver.FindElement(By.Id(fields.AADUsernameInputId)).SendKeys(user.Upn.Contains("EXT") ? user.HomeUPN : user.Upn); + + Trace.WriteLine("Logging in ... Clicking after user name"); + + driver.FindElement(By.Id(fields.AADSignInButtonId)).Click(); + } + + private static void EnterPassword( + IWebDriver driver, + LabUser user, + UserInformationFieldIds fields) + { + Trace.WriteLine("Logging in ... Entering password"); + string password = user.GetOrFetchPassword(); + string passwordField = fields.GetPasswordInputId(); + driver.FindElement(By.Id(passwordField)).SendKeys(password); + + Trace.WriteLine("Logging in ... Clicking next after password"); + driver.FindElement(By.Id(fields.GetPasswordSignInButtonId())).Click(); + } + + private static void HandleStaySignedInPrompt(IWebDriver driver) + { + Trace.WriteLine("Logging in ... Clicking 'No' for staying signed in"); + var acceptBtn = driver.FindElement(By.Id(TestConstants.StaySignedInNoId)); + acceptBtn?.Click(); + } + } +#endif //FROM_GITHUB_ACTION +} diff --git a/tests/IntegrationTests/WebAppUiTests/WebAppUiTests.csproj b/tests/IntegrationTests/WebAppUiTests/WebAppUiTests.csproj new file mode 100644 index 000000000..d199a5736 --- /dev/null +++ b/tests/IntegrationTests/WebAppUiTests/WebAppUiTests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp3.1; net5.0 + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs index be117c809..c7c5c0109 100644 --- a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs @@ -128,5 +128,14 @@ public static class TestConstants public const string SecurePageCallDownstreamWebApiGeneric = "/SecurePage/CallDownstreamWebApiGenericAsync"; public const string SecurePageCallMicrosoftGraph = "/SecurePage/CallMicrosoftGraph"; public const string SectionNameCalledApi = "CalledApi"; + + // Selenium Automation + public const string WebSubmitId = "idSIButton9"; + public const string WebUPNInputId = "i0116"; + public const string WebPasswordId = "i0118"; + public const string ConsentAcceptId = "idBtn_Accept"; + public const string StaySignedInNoId = "idBtn_Back"; + public const string PhotoLabel = "photo"; + public const string Headless = "headless"; } } diff --git a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs index 908465b64..cb81f4687 100644 --- a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs +++ b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs @@ -43,7 +43,6 @@ public async Task GetTokenForUserAsync( bool addInMemoryTokenCache = true) { // Arrange - IServiceProvider serviceProvider = null; var client = _factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => @@ -62,7 +61,7 @@ public async Task GetTokenForUserAsync( #pragma warning restore CS0618 // Type or member is obsolete } - serviceProvider = services.BuildServiceProvider(); + services.BuildServiceProvider(); }); }) .CreateClient(new WebApplicationFactoryClientOptions diff --git a/tests/Microsoft.Identity.Web.Test.LabInfrastructure/UserInformationFieldIds.cs b/tests/Microsoft.Identity.Web.Test.LabInfrastructure/UserInformationFieldIds.cs new file mode 100644 index 000000000..48ce03c66 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test.LabInfrastructure/UserInformationFieldIds.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web.Test.Common; + +namespace Microsoft.Identity.Web.Test.LabInfrastructure +{ + public class UserInformationFieldIds + { + private string _passwordInputId; + private string _passwordSignInButtonId; + + public string GetPasswordInputId() + { + if (string.IsNullOrWhiteSpace(_passwordInputId)) + { + DetermineFieldIds(); + } + + return _passwordInputId; + } + + public string GetPasswordSignInButtonId() + { + if (string.IsNullOrWhiteSpace(_passwordSignInButtonId)) + { + DetermineFieldIds(); + } + + return _passwordSignInButtonId; + } + + /// + /// When starting auth, the first screen, which collects the username, is from AAD. + /// + public string AADSignInButtonId + { + get + { + return TestConstants.WebSubmitId; + } + } + + /// + /// When starting auth, the first screen, which collects the username, is from AAD. + /// + public string AADUsernameInputId + { + get + { + return TestConstants.WebUPNInputId; + } + } + + private void DetermineFieldIds() + { + _passwordInputId = TestConstants.WebPasswordId; + _passwordSignInButtonId = TestConstants.WebSubmitId; + } + } +} diff --git a/tests/WebAppCallsMicrosoftGraph/.config/dotnet-tools.json b/tests/WebAppCallsMicrosoftGraph/.config/dotnet-tools.json new file mode 100644 index 000000000..e53f86600 --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "3.1.7", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/webAppTestMsIdWeb - Web Deploy/profile.arm.json b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/webAppTestMsIdWeb - Web Deploy/profile.arm.json new file mode 100644 index 000000000..50deb69fe --- /dev/null +++ b/tests/WebAppCallsMicrosoftGraph/Properties/ServiceDependencies/webAppTestMsIdWeb - Web Deploy/profile.arm.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_dependencyType": "appService.windows" + }, + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "MsIdWeb", + "metadata": { + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "centralus", + "metadata": { + "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." + } + }, + "resourceName": { + "type": "string", + "defaultValue": "webAppTestMsIdWeb", + "metadata": { + "description": "Name of the main resource to be created by this template." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "variables": { + "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "location": "[parameters('resourceLocation')]", + "name": "[parameters('resourceName')]", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "tags": { + "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" + }, + "dependsOn": [ + "[variables('appServicePlan_ResourceId')]" + ], + "kind": "app", + "properties": { + "name": "[parameters('resourceName')]", + "kind": "app", + "httpsOnly": true, + "reserved": false, + "serverFarmId": "[variables('appServicePlan_ResourceId')]", + "siteConfig": { + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnetcore" + } + ] + } + }, + "identity": { + "type": "SystemAssigned" + } + }, + { + "location": "[parameters('resourceLocation')]", + "name": "[variables('appServicePlan_name')]", + "type": "Microsoft.Web/serverFarms", + "apiVersion": "2015-08-01", + "sku": { + "name": "S1", + "tier": "Standard", + "family": "S", + "size": "S1" + }, + "properties": { + "name": "[variables('appServicePlan_name')]" + } + } + ] + } + } + } + ] +} \ No newline at end of file