Skip to content

Commit 045b4ed

Browse files
authored
Device Code Flow Selenium tests (#718)
1 parent 6a7850e commit 045b4ed

File tree

9 files changed

+327
-7
lines changed

9 files changed

+327
-7
lines changed

LibsNoSamples.sln

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Test.Sid
2525
EndProject
2626
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client.Ref", "src\Microsoft.Identity.Client.Ref\Microsoft.Identity.Client.Ref.csproj", "{528C5488-C0D8-45C3-BEAC-3E52C22631C9}"
2727
EndProject
28+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{528F34D9-FF6B-4AAE-8367-182EC19DAB5F}"
29+
EndProject
30+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Test.Core.UIAutomation", "tests\Microsoft.Identity.Test.Core.UIAutomation\Microsoft.Identity.Test.Core.UIAutomation.csproj", "{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4}"
31+
EndProject
2832
Global
2933
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3034
Debug|Any CPU = Debug|Any CPU
@@ -59,6 +63,10 @@ Global
5963
{528C5488-C0D8-45C3-BEAC-3E52C22631C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
6064
{528C5488-C0D8-45C3-BEAC-3E52C22631C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
6165
{528C5488-C0D8-45C3-BEAC-3E52C22631C9}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4}.Release|Any CPU.Build.0 = Release|Any CPU
6270
EndGlobalSection
6371
GlobalSection(SolutionProperties) = preSolution
6472
HideSolutionNode = FALSE
@@ -69,6 +77,8 @@ Global
6977
{30287EC8-BD85-41CC-BDCB-9DBF9EFBCC98} = {5D231979-0A13-456D-B9F9-CED41F935FD9}
7078
{E22918A8-8FE6-47D2-B65E-34436918C5D7} = {5D231979-0A13-456D-B9F9-CED41F935FD9}
7179
{D3033A40-F2C2-4495-BC9F-C3C6D497D1CC} = {5D231979-0A13-456D-B9F9-CED41F935FD9}
80+
{528C5488-C0D8-45C3-BEAC-3E52C22631C9} = {528F34D9-FF6B-4AAE-8367-182EC19DAB5F}
81+
{3DC6EC76-D350-4D43-B206-A4CEC8AA36D4} = {5D231979-0A13-456D-B9F9-CED41F935FD9}
7282
EndGlobalSection
7383
GlobalSection(ExtensibilityGlobals) = postSolution
7484
SolutionGuid = {6C88EF77-AF03-4F82-B666-1831DABAA119}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//----------------------------------------------------------------------
2+
//
3+
// Copyright (c) Microsoft Corporation.
4+
// All rights reserved.
5+
//
6+
// This code is licensed under the MIT License.
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files(the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions :
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
//------------------------------------------------------------------------------
27+
28+
namespace Microsoft.Identity.Test.Common
29+
{
30+
public static class TestCategories
31+
{
32+
/// <summary>
33+
/// Tests under this category use a Selenium driven browser (Chrome) to automate the web ui.
34+
/// When run in the lab, the browser is configured to run headless.
35+
/// For debugging, consider running with the actual browser.
36+
/// </summary>
37+
public const string Selenium = "Selenium";
38+
public const string LabAccess = "LabAccess";
39+
40+
}
41+
}

tests/Microsoft.Identity.Test.LabInfrastructure/LabUser.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ public LabUser() { }
7979
[JsonProperty("b2cProvider")]
8080
public B2CIdentityProvider B2CIdentityProvider { get; set; }
8181

82+
83+
private string _password = null;
84+
85+
public string Password
86+
{
87+
get
88+
{
89+
if (_password == null)
90+
{
91+
_password = LabUserHelper.GetUserPassword(this);
92+
}
93+
94+
return _password;
95+
}
96+
}
97+
8298
public void InitializeHomeUser()
8399
{
84100
HomeUser = new LabUser();

tests/Microsoft.Identity.Test.LabInfrastructure/LabUserHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
//------------------------------------------------------------------------------
2727

2828
using System;
29+
using System.Diagnostics;
2930

3031
namespace Microsoft.Identity.Test.LabInfrastructure
3132
{

tests/Microsoft.Identity.Test.Unit.net45/AuthorityAliasesTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ internal void TestInitialize(MockHttpManager httpManager)
6262
[Description("Test authority migration")]
6363
public void AuthorityMigration_IntegrationTest()
6464
{
65-
// make sure that for all network calls "preferred_cache" enironment is used
65+
// make sure that for all network calls "preferred_cache" environment is used
6666
// (it is taken from metadata in instance discovery response),
6767
// except very first network call - instance discovery
6868

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//------------------------------------------------------------------------------
2+
//
3+
// Copyright (c) Microsoft Corporation.
4+
// All rights reserved.
5+
//
6+
// This code is licensed under the MIT License.
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files(the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions :
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
//
26+
//------------------------------------------------------------------------------
27+
using System;
28+
using System.Diagnostics;
29+
using System.Net;
30+
using System.Threading.Tasks;
31+
using Microsoft.Identity.Client;
32+
using Microsoft.Identity.Test.Common;
33+
using Microsoft.Identity.Test.Core.UIAutomation;
34+
using Microsoft.Identity.Test.LabInfrastructure;
35+
using Microsoft.VisualStudio.TestTools.UnitTesting;
36+
using OpenQA.Selenium;
37+
38+
namespace Microsoft.Identity.Test.Unit.Integration
39+
{
40+
// Important: do not install a NuGet package with the Chrome driver as it is a security risk.
41+
// Instead, install the Chrome driver on the test machine
42+
43+
// Note: these tests require permission to a KeyVault Microsoft account;
44+
// Please ignore them if you are not a Microsoft FTE, they will run as part of the CI build
45+
[TestClass]
46+
[TestCategory(TestCategories.Selenium)]
47+
[TestCategory(TestCategories.LabAccess)]
48+
public class DeviceCodeFlow
49+
{
50+
private static readonly string[] Scopes = { "User.Read" };
51+
private SeleniumWrapper _seleniumWrapper;
52+
53+
#region MSTest Hooks
54+
/// <summary>
55+
/// Initialized by MSTest (do not make private or readonly)
56+
/// </summary>
57+
public TestContext TestContext { get; set; }
58+
59+
[ClassInitialize]
60+
public static void ClassInitialize(TestContext context)
61+
{
62+
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
63+
}
64+
65+
[TestInitialize]
66+
public void TestInitialize()
67+
{
68+
TestCommon.ResetState();
69+
_seleniumWrapper = new SeleniumWrapper();
70+
}
71+
72+
[TestCleanup]
73+
public void Cleanup()
74+
{
75+
_seleniumWrapper?.Dispose();
76+
}
77+
#endregion
78+
79+
[TestMethod]
80+
[Timeout(1 * 60 * 1000)] // 1 min timeout
81+
public async Task DeviceCodeFlowTestAsync()
82+
{
83+
LabResponse labResponse = LabUserHelper.GetDefaultUser();
84+
85+
Trace.WriteLine("Calling AcquireTokenWithDeviceCodeAsync");
86+
PublicClientApplication pca = new PublicClientApplication(labResponse.AppId);
87+
var result = await pca.AcquireTokenWithDeviceCodeAsync(Scopes, deviceCodeResult =>
88+
{
89+
RunAutomatedDeviceCodeFlow(deviceCodeResult, labResponse.User);
90+
91+
return Task.FromResult(0);
92+
}).ConfigureAwait(false);
93+
94+
Trace.WriteLine("Running asserts");
95+
96+
Assert.IsNotNull(result);
97+
Assert.IsTrue(!string.IsNullOrEmpty(result.AccessToken));
98+
}
99+
100+
private void RunAutomatedDeviceCodeFlow(DeviceCodeResult deviceCodeResult, LabUser user)
101+
{
102+
IWebDriver driver = _seleniumWrapper.Driver;
103+
try
104+
{
105+
Trace.WriteLine("Browser is open. Navigating to the Device Code url and entering the code");
106+
107+
driver.Navigate().GoToUrl(deviceCodeResult.VerificationUrl);
108+
driver.FindElement(By.Id("code")).SendKeys(deviceCodeResult.UserCode);
109+
110+
IWebElement continueBtn = _seleniumWrapper.WaitForElementToBeVisibleAndEnabled(By.Id("continueBtn"));
111+
continueBtn?.Click();
112+
113+
PerformLogin(user);
114+
115+
Trace.WriteLine("Authentication complete");
116+
117+
}
118+
catch (Exception ex)
119+
{
120+
Trace.WriteLine("Browser automation failed " + ex);
121+
_seleniumWrapper.SaveScreenshot(TestContext);
122+
throw;
123+
}
124+
}
125+
126+
private void PerformLogin(LabUser user)
127+
{
128+
UserInformationFieldIds fields = new UserInformationFieldIds();
129+
fields.DetermineFieldIds(user);
130+
131+
IWebDriver driver = _seleniumWrapper.Driver;
132+
133+
Trace.WriteLine("Logging in ... Entering username");
134+
driver.FindElement(By.Id(CoreUiTestConstants.WebUPNInputID)).SendKeys(user.Upn);
135+
136+
Trace.WriteLine("Logging in ... Clicking next after username");
137+
driver.FindElement(By.Id(fields.SignInButtonId)).Click();
138+
139+
Trace.WriteLine("Logging in ... Entering password");
140+
_seleniumWrapper.WaitForElementToBeVisibleAndEnabled(By.Id(fields.PasswordInputId)).SendKeys(user.Password);
141+
142+
Trace.WriteLine("Logging in ... Clicking next after password");
143+
_seleniumWrapper.WaitForElementToBeVisibleAndEnabled(By.Id(fields.SignInButtonId)).Click();
144+
}
145+
}
146+
}

tests/Microsoft.Identity.Test.Unit.net45/Microsoft.Identity.Test.Unit.net45.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,16 @@
3030
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
3131
<PackageReference Include="MSTest.TestAdapter" Version="1.3.1" />
3232
<PackageReference Include="MSTest.TestFramework" Version="1.3.1" />
33+
<PackageReference Include="StrongNamer" Version="0.0.8" />
3334
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.2.4" />
3435
<!-- This reference is a workaround for a bug in .net46
3536
https://stackoverflow.com/questions/45563560/could-not-load-file-or-assembly-system-net-http-version-4-1-1-1-net-standard-->
3637
<PackageReference Include="System.Net.Http" Version="4.3.3" />
38+
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
39+
40+
<Compile Include="../Microsoft.Identity.Test.Core.UIAutomation/UserInformationFieldIds.cs" LinkBase="Integration"></Compile>
41+
<Compile Include="../Microsoft.Identity.Test.Core.UIAutomation/CoreUiTestConstants.cs" LinkBase="Integration"></Compile>
42+
3743

3844
<Compile Remove="Resources\**" />
3945
<EmbeddedResource Remove="Resources\**" />
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using OpenQA.Selenium;
2+
using System;
3+
using System.IO;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using OpenQA.Selenium.Chrome;
6+
using OpenQA.Selenium.Support.UI;
7+
using System.Diagnostics;
8+
9+
namespace Microsoft.Identity.Test.Unit
10+
{
11+
public class SeleniumWrapper : IDisposable
12+
{
13+
public SeleniumWrapper()
14+
{
15+
ChromeOptions chromeOptions = new ChromeOptions();
16+
17+
// ~2x faster, no visual rendering
18+
// remove when debugging to see the UI automation
19+
chromeOptions.AddArguments("headless");
20+
21+
Driver = new ChromeDriver(chromeOptions);
22+
Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
23+
}
24+
25+
public IWebDriver Driver { get; }
26+
27+
private int _picNumber = 1;
28+
29+
public void SaveScreenshot(TestContext testContext, string name = "failure")
30+
{
31+
Screenshot ss = ((ITakesScreenshot)Driver).GetScreenshot();
32+
string picName = name + _picNumber++ + ".png";
33+
#if DESKTOP // Can't attach a file on netcore because mstest doesn't support it
34+
string failurePicturePath = Path.Combine(testContext.TestResultsDirectory, picName);
35+
#else
36+
string failurePicturePath = Path.Combine(Directory.GetCurrentDirectory(), picName);
37+
#endif
38+
39+
Trace.WriteLine($"Saving picture to {failurePicturePath}");
40+
ss.SaveAsFile(failurePicturePath, ScreenshotImageFormat.Png);
41+
42+
#if DESKTOP // Can't attach a file to the logs on netcore because mstest doesn't support it
43+
testContext.AddResultFile(failurePicturePath);
44+
#endif
45+
}
46+
47+
public IWebElement WaitForElementToBeVisibleAndEnabled(By by)
48+
{
49+
WebDriverWait webDriverWait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
50+
IWebElement continueBtn = webDriverWait.Until(dr =>
51+
{
52+
try
53+
{
54+
var elementToBeDisplayed = Driver.FindElement(by);
55+
if (elementToBeDisplayed.Displayed && elementToBeDisplayed.Enabled)
56+
{
57+
return elementToBeDisplayed;
58+
}
59+
return null;
60+
}
61+
catch (StaleElementReferenceException)
62+
{
63+
return null;
64+
}
65+
catch (NoSuchElementException)
66+
{
67+
return null;
68+
}
69+
});
70+
return continueBtn;
71+
}
72+
73+
#region IDisposable Support
74+
private bool _disposedValue; // To detect redundant calls
75+
76+
protected virtual void Dispose(bool disposing)
77+
{
78+
if (!_disposedValue)
79+
{
80+
if (disposing)
81+
{
82+
// dispose managed state (managed objects).
83+
Driver?.Dispose();
84+
}
85+
86+
_disposedValue = true;
87+
}
88+
}
89+
90+
// This code added to correctly implement the disposable pattern.
91+
public void Dispose()
92+
{
93+
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
94+
Dispose(true);
95+
}
96+
#endregion
97+
}
98+
}

0 commit comments

Comments
 (0)