Skip to content

Commit

Permalink
feat: test data setup as named user (#58)
Browse files Browse the repository at this point in the history
+semver: feature
  • Loading branch information
ewingjm committed Jan 13, 2021
1 parent c8dc3f0 commit 1753cd0
Show file tree
Hide file tree
Showing 39 changed files with 1,020 additions and 263 deletions.
55 changes: 40 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ The aim of this project is to make Power Apps test automation easier, faster and
- [Writing feature files](#Writing-feature-files)
- [Writing step bindings](#Writing-step-bindings)
- [Test setup](#Test-setup)
- [Bindings](#Bindings)
- [Data file syntax](#Data-file-syntax)
- [Dynamic data](#Dynamic-data)
- [Contributing](#Contributing)
- [Licence](#Licence)

Expand Down Expand Up @@ -50,20 +53,24 @@ PM> Install-Package Selenium.Chrome.WebDriver
Installing the NuGet package creates a _power-apps-bindings.yml_ file in your project's root. This is used to configure the URL, browser, and users that will be used for your tests.

```yaml
url: SPECFLOW_POWERAPPS_URL
browserOptions:
url: SPECFLOW_POWERAPPS_URL # mandatory
browserOptions: # optional - will use default EasyRepro options if not set
browserType: Chrome
headless: true
width: 1920
height: 1080
startMaximized: false
users:
- username: SPECFLOW_POWERAPPS_USERNAME_SALESPERSON
password: SPECFLOW_POWERAPPS_PASSWORD_SALESPERSON
alias: a salesperson
applicationUser: # optional - populate if creating test data for users other than the current user
tenantId: SPECFLOW_POWERAPPS_TENANTID optional # mandatory
clientId: SPECFLOW_POWERAPPS_CLIENTID # mandatory
clientSecret: SPECFLOW_POWERAPPS_CLIENTSECRET # mandatory
users: # mandatory
- username: SPECFLOW_POWERAPPS_USERNAME_SALESPERSON # mandatory
password: SPECFLOW_POWERAPPS_PASSWORD_SALESPERSON # optional - populate if this user will be logging in for tests
alias: a salesperson # mandatory
```
The URL, usernames, and passwords 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.
The URL, 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.

### Writing feature files

Expand Down Expand Up @@ -97,20 +104,30 @@ public class MyCustomSteps : PowerAppsStepDefiner

### Test setup

#### Bindings

We avoid performing test setup via the UI. This speeds up test execution and makes the tests more robust (as UI automation is more fragile than using supported APIs). _Given_ steps should therefore be carried out using the [Client API](client-api), [WebAPI](web-api) or [Organization Service](org-service).

You can create test data by using the following _Given_ step -
You can create test data by using the following _Given_ steps -

```gherkin
Given I have created "a record"
Given I have created 'a record'
```
or
```gherkin
Given 'someone' has created 'a record'
```

This will look for a JSON file named _a record.json_ in the _data_ folder (you must ensure that these files are copying to the build output directory). The JSON is the same as expected by WebAPI when creating records via a [deep insert](https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/create-entity-web-api#create-related-entities-in-one-operation). The example below will create the following -
The examples above will both look for a JSON file named _a record.json_ in the _data_ folder (you must ensure that these files are copying to the build output directory). The difference is that the latter requires the following in the configuration file:

- An account
- An primary contact related to the account
- An opportunity related to the account
- A task related to the opportunity
- a user with an alias of _someone_ in the `users` array
- an application user with sufficient privileges to impersonate the above user configured in the `applicationUser` property.

Refer to the Microsoft documentation on creating an application user [here](https://docs.microsoft.com/en-us/power-platform/admin/create-users-assign-online-security-roles#create-an-application-user).

#### Data file syntax

The JSON is similar to that expected by Web API when creating records via a [deep insert](https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/create-entity-web-api#create-related-entities-in-one-operation).

```json
{
Expand All @@ -129,10 +146,16 @@ This will look for a JSON file named _a record.json_ in the _data_ folder (you m
]
}
```
The example above will create the following:

- An account
- An primary contact related to the account
- An opportunity related to the account
- A task related to the opportunity

The `@logicalName` property is required for the root record.

The `@alias` property can optionally be added to any record and allows the record to be referenced in certain bindings. The _Given I have created_ binding itself supports relating records using `@alias.bind` syntax.
The `@alias` property can optionally be added to any record and allows the record to be referenced in certain bindings. The _Given I have created_ binding itself supports relating records using `@alias.bind` syntax, as shown below:

```json
{
Expand All @@ -143,6 +166,8 @@ The `@alias` property can optionally be added to any record and allows the recor
}
```

#### Dynamic data

We also support the use of
[faker.js](https://github.com/marak/Faker.js) moustache template syntax for generating dynamic test data at run-time. Please refer to the faker documentation for all of the functionality that is available.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.29" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.24.0" />
<PackageReference Include="Microsoft.SourceLink.Common" Version="1.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Configuration
{
using YamlDotNet.Serialization;

/// <summary>
/// Configuration for the test application user.
/// </summary>
public class ClientCredentials
{
private string tenantId;
private string clientId;
private string clientSecret;

/// <summary>
/// Gets or sets the tenant ID of the test application user app registration.
/// </summary>
[YamlMember(Alias = "tenantId")]
public string TenantId { get => ConfigHelper.GetEnvironmentVariableIfExists(this.tenantId); set => this.tenantId = value; }

/// <summary>
/// Gets or sets the client ID of the test application user app registration.
/// </summary>
[YamlMember(Alias = "clientId")]
public string ClientId { get => ConfigHelper.GetEnvironmentVariableIfExists(this.clientId); set => this.clientId = value; }

/// <summary>
/// Gets or sets a client secret or the name of an environment variable containing the client secret of the test application user app registration.
/// </summary>
[YamlMember(Alias = "clientSecret")]
public string ClientSecret { get => ConfigHelper.GetEnvironmentVariableIfExists(this.clientSecret); set => this.clientSecret = value; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Configuration
{
using System;

/// <summary>
/// Helper methods for configuration classes.
/// </summary>
public static class ConfigHelper
{
/// <summary>
/// Returns the value of an environment variable if it exists. Alternatively, returns the passed in value.
/// </summary>
/// <param name="value">The value which may be the name of an environment variable.</param>
/// <returns>The environment variable value (if found) or the passed in value.</returns>
public static string GetEnvironmentVariableIfExists(string value)
{
var environmentVariableValue = Environment.GetEnvironmentVariable(value);

if (!string.IsNullOrEmpty(environmentVariableValue))
{
return environmentVariableValue;
}

return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,37 @@ public TestConfiguration()
this.BrowserOptions = new BrowserOptions();
}

#pragma warning disable CA1056 // Uri properties should not be strings

/// <summary>
/// Gets or sets the URL of the target Dynamics 365 instance.
/// Sets the URL of the target Dynamics 365 instance.
/// </summary>
[YamlMember(Alias = "url")]
public string Url { get; set; }

#pragma warning restore CA1056 // Uri properties should not be strings
public string Url { private get; set; }

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

#pragma warning disable CA2227 // Collection properties should be read only

/// <summary>
/// Gets or sets users that tests can be run as.
/// </summary>
[YamlMember(Alias = "users")]
public List<UserConfiguration> Users { get; set; }

#pragma warning restore CA2227 // Collection properties should be read only
/// <summary>
/// Gets or sets application user client ID and client secret for performing certain operations (e.g. impersonating other users during test data creation).
/// </summary>
[YamlMember(Alias = "applicationUser")]
public ClientCredentials ApplicationUser { get; set; }

/// <summary>
/// Gets the target URL.
/// </summary>
/// <returns>The URL of the test environment.</returns>
public Uri GetTestUrl()
{
var urlEnvironmentVariable = Environment.GetEnvironmentVariable(this.Url);

return string.IsNullOrEmpty(urlEnvironmentVariable) ? new Uri(this.Url) : new Uri(urlEnvironmentVariable);
return new Uri(ConfigHelper.GetEnvironmentVariableIfExists(this.Url));
}

/// <summary>
Expand All @@ -73,15 +69,7 @@ public UserConfiguration GetUser(string userAlias)
{
try
{
var user = this.Users.First(u => u.Alias == userAlias);

var usernameEnvironmentVariable = Environment.GetEnvironmentVariable(user.Username);
user.Username = string.IsNullOrEmpty(usernameEnvironmentVariable) ? user.Username : usernameEnvironmentVariable;

var passwordEnvironmentVariable = Environment.GetEnvironmentVariable(user.Password);
user.Password = string.IsNullOrEmpty(passwordEnvironmentVariable) ? user.Password : passwordEnvironmentVariable;

return user;
return this.Users.First(u => u.Alias == userAlias);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
/// </summary>
public class UserConfiguration
{
private string username;
private string password;

/// <summary>
/// Gets or sets the username of the user.
/// </summary>
[YamlMember(Alias = "username")]
public string Username { get; set; }
public string Username { get => ConfigHelper.GetEnvironmentVariableIfExists(this.username); set => this.username = value; }

/// <summary>
/// Gets or sets the password of the user.
/// </summary>
[YamlMember(Alias = "password")]
public string Password { get; set; }
public string Password { get => ConfigHelper.GetEnvironmentVariableIfExists(this.password); set => this.password = value; }

/// <summary>
/// Gets or sets the alias of the user (used to retrieve from configuration).
Expand Down
13 changes: 13 additions & 0 deletions bindings/src/Capgemini.PowerApps.SpecFlowBindings/ITestDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
/// </summary>
public interface ITestDriver
{
/// <summary>
/// Injects the driver onto the current page.
/// </summary>
/// <param name="authToken">The application user auth token (if configured).</param>
void InjectOnPage(string authToken);

/// <summary>
/// Loads scenario test data.
/// </summary>
/// <param name="data">The data to load.</param>
/// <param name="username">The username of the user to impersonate.</param>
void LoadTestDataAsUser(string data, string username);

/// <summary>
/// Loads scenario test data.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
namespace Capgemini.PowerApps.SpecFlowBindings
{
using System;
using System.Configuration;
using System.IO;
using System.Reflection;
using Capgemini.PowerApps.SpecFlowBindings.Configuration;
using Microsoft.Dynamics365.UIAutomation.Api.UCI;
using Microsoft.Dynamics365.UIAutomation.Browser;
using Microsoft.Identity.Client;
using OpenQA.Selenium;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
Expand All @@ -17,6 +18,8 @@ public abstract class PowerAppsStepDefiner
{
private static TestConfiguration testConfig;

private static IConfidentialClientApplication app;

[ThreadStatic]
private static ITestDriver testDriver;

Expand All @@ -29,6 +32,21 @@ public abstract class PowerAppsStepDefiner
[ThreadStatic]
private static XrmApp xrmApp;

/// <summary>
/// Gets access token used to authenticate as the application user configured for testing.
/// </summary>
protected static string AccessToken
{
get
{
var hostSegments = TestConfig.GetTestUrl().Host.Split('.');

return GetApp().AcquireTokenForClient(new string[] { $"https://{hostSegments[0]}.api.{hostSegments[1]}.dynamics.com//.default" })
.ExecuteAsync()
.Result.AccessToken;
}
}

/// <summary>
/// Gets the configuration for the test project.
/// </summary>
Expand Down Expand Up @@ -82,7 +100,19 @@ protected static TestConfiguration TestConfig
/// <summary>
/// Gets provides utilities for test setup/teardown.
/// </summary>
protected static ITestDriver TestDriver => testDriver ?? (testDriver = new TestDriver((IJavaScriptExecutor)Driver));
protected static ITestDriver TestDriver
{
get
{
if (testDriver == null)
{
testDriver = new TestDriver((IJavaScriptExecutor)Driver);
testDriver.InjectOnPage(TestConfig.ApplicationUser != null ? AccessToken : null);
}

return testDriver;
}
}

/// <summary>
/// Performs any cleanup necessary when quitting the WebBrowser.
Expand All @@ -99,5 +129,26 @@ protected static void Quit()
client = null;
testDriver = null;
}

/// <summary>
/// Gets the <see cref="IConfidentialClientApplication"/> used to authenticate as the configured application user.
/// </summary>
private static IConfidentialClientApplication GetApp()
{
if (TestConfig.ApplicationUser == null)
{
throw new ConfigurationErrorsException("An application user has not been configured.");
}

if (app == null)
{
app = ConfidentialClientApplicationBuilder.Create(TestConfig.ApplicationUser.ClientId)
.WithTenantId(TestConfig.ApplicationUser.TenantId)
.WithClientSecret(TestConfig.ApplicationUser.ClientSecret)
.Build();
}

return app;
}
}
}
Loading

0 comments on commit 1753cd0

Please sign in to comment.