Skip to content
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

feat: allow bypassing/streamlining of login using browser profiles #87

Merged
merged 41 commits into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fecaac5
Inital logic commit to enable use of Chrome Profiles
Jun 15, 2021
578f047
Refactored work to apply to both Firefox & Chrome. Added Workaround f…
Jun 15, 2021
5851084
Tidied up and added switch to disable/enable the use of profiles
Jun 15, 2021
19f79c6
Reverted project version used in testing
Jun 15, 2021
e135286
Updated method name to not specifically refer to chrome
Jun 15, 2021
83d052c
Fixed a few naming &formatting issues
Jun 15, 2021
7aa4ce6
Updated inline with review comments
Jun 16, 2021
2772a08
Refactored custom login logic
Jun 16, 2021
836e0f1
Tweaked connection string
Jun 16, 2021
c28fa91
Removed debugging steps
Jun 16, 2021
28a52a9
Reworked the setting of userProfileDirectories
Jun 16, 2021
f116ad5
Removed logging
Jun 16, 2021
5b61846
Removed WaitForTransaction calls
Jun 16, 2021
26669ed
Updated readme
Jun 16, 2021
d94c733
Added logic to create a copy of the chrome profile for each test - WIP
Jun 18, 2021
84acbb1
Updated to reduce the length of the profile file paths
Jun 18, 2021
36a450c
Added hook to cleanup used profiles
Jun 18, 2021
7c252ba
Switched to use the default dir for profiles when building in ADO
Jun 18, 2021
d87365b
Fixed issue where variable wasn't set
Jun 18, 2021
4029d54
Added polly to retry the folder delete - as no way to tell when the d…
Jun 18, 2021
9c604fe
Updated after scenario hooks to handle errors
Jun 21, 2021
9da076c
Updated to use events rather than run order
Jun 21, 2021
4f03282
Updated to give up deleting profile folder after 4 attempts
Jun 21, 2021
cb6739a
Disabled the use of profiles - testing ADO pipeline
Jun 21, 2021
be67e0f
fix: multi-threading issues
ewingjm Jun 22, 2021
deb1939
Fixed errors when running tests locally
Jun 22, 2021
ab44036
Fixed issue with config file
Jun 22, 2021
6870368
fix: issue with locking and getting environment variables
Jun 22, 2021
b641940
fix: issues deleting profile directories
ewingjm Jun 22, 2021
27ef9ee
fix: condition for deleting profile directory
ewingjm Jun 22, 2021
eb8bacb
fix: errors copying and deleting profile directories
ewingjm Jun 22, 2021
b6c7296
ci: fix issues copying to new profile folder
ewingjm Jun 23, 2021
4786d4b
fix: force headless during profile setup
Jun 23, 2021
de7cf9b
fix: remove unnecessary lock
ewingjm Jun 23, 2021
229cbb9
Merge branch 'AB/bypass-login' of https://github.com/bancey/powerapps…
ewingjm Jun 23, 2021
c73ede7
fix: reported code smell
Jun 23, 2021
64b3e62
Resolved build issue
Jun 23, 2021
d789047
fix: issues disposing driver
ewingjm Jun 23, 2021
8ee2af8
ci: ignore test failing due to EasyRepro issue
ewingjm Jun 23, 2021
7571a41
docs: fix Markdown linting issues in README
ewingjm Jun 24, 2021
7ac87f6
fix: creates user profile for users without passwords
ewingjm Jun 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Installing the NuGet package creates a _power-apps-bindings.yml_ file in your pr

```yaml
url: SPECFLOW_POWERAPPS_URL # mandatory
useProfiles: false # defaults to false if not provided
ewingjm marked this conversation as resolved.
Show resolved Hide resolved
browserOptions: # optional - will use default EasyRepro options if not set
browserType: Chrome
headless: true
Expand All @@ -73,6 +74,9 @@ users: # mandatory

The URL, driversPath, usernames, passwords, and application user details will be set from environment variable (if found). Otherwise, the value from the config file will be used. The browserOptions node supports anything in the EasyRepro `BrowserOptions` class.

#### User Profiles
ewingjm marked this conversation as resolved.
Show resolved Hide resolved
Setting the `useProfiles` property to true causes the solution to create and use a unique [profile](https://support.google.com/chrome/answer/2364824?co=GENIE.Platform%3DDesktop&hl=en) for each user listed in the config file. This currently only works in Chrome & Firefox and attempting to use it with Edge or IE will cause an exception to be thrown. By using profiles test runs for the same user will not be required to re-authenticate, this saves time during test runs. [To take full advantage of this you will need to have the "Stay Signed In" prompt enabled.](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/keep-me-signed-in)

### Writing feature files

You can use the predefined step bindings to write your tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Polly" Version="7.2.2" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="SpecFlow" Version="3.5.14" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Configuration
{
using System.IO;
using Microsoft.Dynamics365.UIAutomation.Browser;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;

/// <summary>
/// Extends the EasyRepro <see cref="BrowserOptions"/> class with additonal support for chrome profiles.
/// </summary>
public class BrowserOptionsWithProfileSupport : BrowserOptions
{
/// <summary>
/// Gets or sets the directory to use as the user profile.
/// </summary>
public string ProfileDirectory { get; set; }

/// <inheritdoc/>
public override ChromeOptions ToChrome()
{
var options = base.ToChrome();
if (!string.IsNullOrEmpty(this.ProfileDirectory))
{
options.AddArgument($"--user-data-dir={this.ProfileDirectory}");
bancey marked this conversation as resolved.
Show resolved Hide resolved
}

return options;
}

/// <inheritdoc/>
public override FirefoxOptions ToFireFox()
{
var options = base.ToFireFox();
if (!string.IsNullOrEmpty(this.ProfileDirectory))
{
this.ProfileDirectory = this.ProfileDirectory.EndsWith("firefox") ? this.ProfileDirectory : Path.Combine(this.ProfileDirectory, "firefox");
options.AddArgument($"-profile \"{this.ProfileDirectory}\"");
}

return options;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using Microsoft.Dynamics365.UIAutomation.Browser;
using YamlDotNet.Serialization;

/// <summary>
Expand All @@ -18,13 +17,13 @@ public class TestConfiguration
public const string FileName = "power-apps-bindings.yml";

private const string GetUserException = "Unable to retrieve user configuration. Please ensure a user with the given alias exists in the config.";

private string profilesBasePath;
/// <summary>
/// Initializes a new instance of the <see cref="TestConfiguration"/> class.
/// </summary>
public TestConfiguration()
{
this.BrowserOptions = new BrowserOptions();
this.BrowserOptions = new BrowserOptionsWithProfileSupport();
}

/// <summary>
Expand All @@ -33,11 +32,23 @@ public TestConfiguration()
[YamlMember(Alias = "url")]
public string Url { private get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use profiles.
/// </summary>
[YamlMember(Alias = "useProfiles")]
public bool UseProfiles { get; set; } = false;

/// <summary>
/// Gets or sets the base path where the user profiles are stored.
/// </summary>
[YamlMember(Alias = "profilesBasePath")]
public string ProfilesBasePath { get => ConfigHelper.GetEnvironmentVariableIfExists(this.profilesBasePath); set => this.profilesBasePath = value; }

/// <summary>
/// Gets or sets the browser options to use for running tests.
/// </summary>
[YamlMember(Alias = "browserOptions")]
public BrowserOptions BrowserOptions { get; set; }
public BrowserOptionsWithProfileSupport BrowserOptions { get; set; }

/// <summary>
/// Gets or sets users that tests can be run as.
Expand All @@ -63,17 +74,23 @@ public Uri GetTestUrl()
/// <summary>
/// Retrieves the configuration for a user.
/// </summary>
/// <param name="userAlias">The alias of the user.</param>
/// <param name="userAliasOrUsername">The alias or username of the user.</param>
/// <returns>The user configuration.</returns>
public UserConfiguration GetUser(string userAlias)
public UserConfiguration GetUser(string userAliasOrUsername)
{
try
{
return this.Users.First(u => u.Alias == userAlias);
UserConfiguration user = this.Users.FirstOrDefault(u => u.Alias == userAliasOrUsername);
if (user != null)
{
return user;
}

return this.Users.First(u => u.Username == userAliasOrUsername);
}
catch (Exception ex)
{
throw new ConfigurationErrorsException($"{GetUserException} User: {userAlias}", ex);
throw new ConfigurationErrorsException($"{GetUserException} User: {userAliasOrUsername}", ex);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Extensions
{
using Microsoft.Dynamics365.UIAutomation.Browser;

/// <summary>
/// Provides extension methods on <see cref="BrowserType"/>.
/// </summary>
public static class BrowserTypeExtensions
{
/// <summary>
/// Determines if the given browser type supports profiles.
/// </summary>
/// <param name="type">The <see cref="BrowserType"/> to check.</param>
/// <returns>true if the browser supports profiles otherwise false.</returns>
public static bool SupportsProfiles(this BrowserType type)
{
switch (type)
{
case BrowserType.IE:
return false;
case BrowserType.Chrome:
return true;
case BrowserType.Firefox:
return true;
case BrowserType.Edge:
return false;
case BrowserType.Remote:
return false;
default:
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Hooks
{
using Capgemini.PowerApps.SpecFlowBindings.Configuration;
using Microsoft.Dynamics365.UIAutomation.Browser;
using OpenQA.Selenium;
using Polly;
using System;
using System.IO;
using System.Reflection;
using OpenQA.Selenium;
using TechTalk.SpecFlow;

/// <summary>
Expand Down Expand Up @@ -52,6 +56,7 @@ public void ScreenshotFailedScenario()
{
var rootFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var screenshotsFolder = Path.Combine(rootFolder, "screenshots");
Console.WriteLine(screenshotsFolder);

if (!Directory.Exists(screenshotsFolder))
{
Expand All @@ -64,5 +69,39 @@ public void ScreenshotFailedScenario()
ScreenshotImageFormat.Jpeg);
}
}

/// <summary>
/// Deletes the user profiles used for this scenario.
/// </summary>
[AfterScenario(Order = 0)]
public void CleanUpProfileDirectory()
{
Client.Browser.BrowserDisposing += new EventHandler<EventArgs>(this.TryCleanupProfile);
}

private void TryCleanupProfile(object sender, EventArgs e)
{
BrowserOptionsWithProfileSupport options = (sender as InteractiveBrowser).Options as BrowserOptionsWithProfileSupport;
var retryPolicy = Policy
.Handle<UnauthorizedAccessException>()
.Or<IOException>()
.WaitAndRetry(new[]
{
3.Seconds(),
5.Seconds(),
10.Seconds(),
15.Seconds(),
});
var fallbackPolicy = Policy
.Handle<UnauthorizedAccessException>()
.Or<IOException>()
.Fallback(() =>
{
// Give up trying to delete the profile folder.
Console.WriteLine("Failed to clean up user data dir as the browser has not exited after 33 seconds.");
});
var retryWithFallback = fallbackPolicy.Wrap(retryPolicy);
retryWithFallback.Execute(() => new DirectoryInfo(options.ProfileDirectory).Delete(true));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Hooks
{
using System.IO;
using Capgemini.PowerApps.SpecFlowBindings.Configuration;
using Capgemini.PowerApps.SpecFlowBindings.Steps;
using TechTalk.SpecFlow;

/// <summary>
/// Hooks that run before the start of each run.
/// </summary>
[Binding]
public class BeforeRunHooks : PowerAppsStepDefiner
{
/// <summary>
/// Creates a new folder for the scenario and copies the session/cookies information from previous runs
/// </summary>
[BeforeTestRun]
public static void BaseProfileSetup()
{
if (!TestConfig.UseProfiles)
{
return;
}

foreach (var username in UserProfileDirectories.Keys)
{
var profileDirectory = UserProfileDirectories[username];
if (Directory.GetDirectories(profileDirectory).Length == 0)
{
var baseDirectory = Path.Combine(profileDirectory, "base");
Directory.CreateDirectory(baseDirectory);

TestConfig.BrowserOptions.ProfileDirectory = baseDirectory;

UserConfiguration user = TestConfig.GetUser(username);
LoginSteps.Login(Driver, TestConfig.GetTestUrl(), user.Username, user.Password);
Quit();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
namespace Capgemini.PowerApps.SpecFlowBindings
{
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using Capgemini.PowerApps.SpecFlowBindings.Configuration;
using Capgemini.PowerApps.SpecFlowBindings.Extensions;
using Microsoft.Dynamics365.UIAutomation.Api.UCI;
using Microsoft.Identity.Client;
using OpenQA.Selenium;
Expand Down Expand Up @@ -32,6 +35,8 @@ public abstract class PowerAppsStepDefiner
[ThreadStatic]
private static XrmApp xrmApp;

private static Dictionary<string, string> userProfileDirectories;

/// <summary>
/// Gets access token used to authenticate as the application user configured for testing.
/// </summary>
Expand Down Expand Up @@ -116,6 +121,47 @@ protected static ITestDriver TestDriver
}
}

/// <summary>
/// Gets the directories for the chrome profiles if the brower type is chrome.
/// </summary>
protected static Dictionary<string, string> UserProfileDirectories
{
get
{
if (!testConfig.BrowserOptions.BrowserType.SupportsProfiles())
{
throw new NotSupportedException($"The {testConfig.BrowserOptions.BrowserType} does not support profiles.");
}

if (userProfileDirectories != null)
{
return userProfileDirectories;
}

var directoriesDictionary = new Dictionary<string, string>();

var basePath = string.IsNullOrEmpty(TestConfig.ProfilesBasePath) ? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) : TestConfig.ProfilesBasePath;
string profilesDir = Path.Combine(basePath, "profiles");
Directory.CreateDirectory(profilesDir);

foreach (var username in TestConfig.Users.Select(u => u.Username).ToList())
{
if (directoriesDictionary.ContainsKey(username))
{
continue;
}

var userProfileDir = Path.Combine(profilesDir, username);
Directory.CreateDirectory(userProfileDir);

directoriesDictionary.Add(username, userProfileDir);
}

userProfileDirectories = directoriesDictionary;
return userProfileDirectories;
}
}

/// <summary>
/// Performs any cleanup necessary when quitting the WebBrowser.
/// </summary>
Expand Down
Loading