The NuGridium API allows you to easily integrate the execution of your browser automation tests into your automated
CI/CD pipeline. With NuGridium you can manage which test methods groups to execute on which of your test web environments.
You can trigger these test executions from an Octopus Deployment step, see here for an example.
A Test is part of a Test Run, it is a browser-specific instance of a test that is contained in the uploaded test dll. A test is executed by an agent using the specified browser against the specified test environment (url to test).
A Test Run is a Test Run Configuration that has been triggered to be executed by one or more agents. A Test Run Configuration consists of a set of browser-specific tests, the dll(s) that contains those test names (which is part of an uploaded zip file with the test binaries), a test environment (url to test), an optional category expression (to filter the tests in the dll) and a WebDriver settings file.
A Test Run Configuration consists of a set of browser-specific tests, the dll(s) that contains those test names (which is part of an uploaded zip file with the test binaries), a test environment (url to test), an optional category expression (to filter the tests in the dll) and a WebDriver settings file.
You can conveniently and securely interact with the web api via an HMAC
authentication model based on shared secret keys.
Below is the code you can use in order to interact with the Nugridium API using a shared secret key.
You can use the below implementation of a couple of custom HttpMessageHandler implementations along with HttpClient.
The following code is based on this source.
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace infrastructure
{
public interface IBuildMessageRepresentation
{
string BuildRequestRepresentation(HttpRequestMessage requestMessage);
}
public class CanonicalRepresentationBuilder : IBuildMessageRepresentation
{
public string BuildRequestRepresentation(HttpRequestMessage requestMessage)
{
bool valid = IsRequestValid(requestMessage);
if (!valid)
{
return null;
}
if (!requestMessage.Headers.Date.HasValue)
{
return null;
}
DateTime date = requestMessage.Headers.Date.Value.UtcDateTime;
string md5 = requestMessage.Content == null ||
requestMessage.Content.Headers.ContentMD5 == null ? ""
: Convert.ToBase64String(requestMessage.Content.Headers.ContentMD5);
string httpMethod = requestMessage.Method.Method;
if (!requestMessage.Headers.Contains(Configuration.ClientIdHeader))
{
return null;
}
string clientId = requestMessage.Headers
.GetValues(Configuration.ClientIdHeader).First();
string uri = requestMessage.RequestUri.AbsolutePath.ToLower();
string representation = String.Join("\n", httpMethod,
md5, date.ToString(CultureInfo.InvariantCulture),
clientId, uri);
return representation;
}
private bool IsRequestValid(HttpRequestMessage requestMessage)
{
//for simplicity omitting headers check
return true;
}
}
public interface ICalculteSignature
{
string Signature(string secret, string value);
}
public class HmacSignatureCalculator : ICalculteSignature
{
public string Signature(string secret, string value)
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var valueBytes = Encoding.UTF8.GetBytes(value);
string signature;
using (var hmac = new HMACSHA256(secretBytes))
{
var hash = hmac.ComputeHash(valueBytes);
signature = Convert.ToBase64String(hash);
}
return signature;
}
}
public class HmacSigningHandler : HttpClientHandler
{
private readonly IBuildMessageRepresentation _representationBuilder;
private readonly ICalculteSignature _signatureCalculator;
public string ClientId { get; set; }
public string ApiKey { get; set; }
public HmacSigningHandler(
IBuildMessageRepresentation representationBuilder,
ICalculteSignature signatureCalculator)
{
_representationBuilder = representationBuilder;
_signatureCalculator = signatureCalculator;
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, sslPolicyErrors) => true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
{
if (!request.Headers.Contains(Configuration.ClientIdHeader))
{
request.Headers.Add(Configuration.ClientIdHeader, ClientId);
}
request.Headers.Date = new DateTimeOffset(DateTime.Now, TimeZoneInfo.Local.GetUtcOffset(DateTime.Now));
var representation = _representationBuilder.BuildRequestRepresentation(request);
var secret = ComputeHash(ApiKey, new SHA1CryptoServiceProvider());
string signature = _signatureCalculator.Signature(secret,
representation);
var header = new AuthenticationHeaderValue(Configuration.AuthenticationScheme, signature);
request.Headers.Authorization = header;
return base.SendAsync(request, cancellationToken);
}
private string ComputeHash(string inputData, HashAlgorithm algorithm)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(inputData);
byte[] hashed = algorithm.ComputeHash(inputBytes);
return Convert.ToBase64String(hashed);
}
}
public class RequestContentMd5Handler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
if (request.Content == null)
{
return await base.SendAsync(request, cancellationToken);
}
byte[] content = await request.Content.ReadAsByteArrayAsync();
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(content);
request.Content.Headers.ContentMD5 = hash;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
public class Configuration
{
public const string ClientIdHeader = "X-ApiAuth-ClientId";
public const string AuthenticationScheme = "ApiAuth";
public const int ValidityPeriodInMinutes = 5;
}
}
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace infrastructure
{
public interface IBuildMessageRepresentation
{
string BuildRequestRepresentation(HttpRequestMessage requestMessage);
}
public class CanonicalRepresentationBuilder : IBuildMessageRepresentation
{
public string BuildRequestRepresentation(HttpRequestMessage requestMessage)
{
bool valid = IsRequestValid(requestMessage);
if (!valid)
{
return null;
}
if (!requestMessage.Headers.Date.HasValue)
{
return null;
}
DateTime date = requestMessage.Headers.Date.Value.UtcDateTime;
string md5 = requestMessage.Content == null ||
requestMessage.Content.Headers.ContentMD5 == null ? ""
: Convert.ToBase64String(requestMessage.Content.Headers.ContentMD5);
string httpMethod = requestMessage.Method.Method;
if (!requestMessage.Headers.Contains(Configuration.ClientIdHeader))
{
return null;
}
string clientId = requestMessage.Headers
.GetValues(Configuration.ClientIdHeader).First();
string uri = requestMessage.RequestUri.AbsolutePath.ToLower();
string representation = String.Join("\n", httpMethod,
md5, date.ToString(CultureInfo.InvariantCulture),
clientId, uri);
return representation;
}
private bool IsRequestValid(HttpRequestMessage requestMessage)
{
//for simplicity omitting headers check
return true;
}
}
public interface ICalculteSignature
{
string Signature(string secret, string value);
}
public class HmacSignatureCalculator : ICalculteSignature
{
public string Signature(string secret, string value)
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var valueBytes = Encoding.UTF8.GetBytes(value);
string signature;
using (var hmac = new HMACSHA256(secretBytes))
{
var hash = hmac.ComputeHash(valueBytes);
signature = Convert.ToBase64String(hash);
}
return signature;
}
}
public class HmacSigningHandler : HttpClientHandler
{
private readonly IBuildMessageRepresentation _representationBuilder;
private readonly ICalculteSignature _signatureCalculator;
public string ClientId { get; set; }
public string ApiKey { get; set; }
public HmacSigningHandler(
IBuildMessageRepresentation representationBuilder,
ICalculteSignature signatureCalculator)
{
_representationBuilder = representationBuilder;
_signatureCalculator = signatureCalculator;
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, sslPolicyErrors) => true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
{
if (!request.Headers.Contains(Configuration.ClientIdHeader))
{
request.Headers.Add(Configuration.ClientIdHeader, ClientId);
}
request.Headers.Date = new DateTimeOffset(DateTime.Now, TimeZoneInfo.Local.GetUtcOffset(DateTime.Now));
var representation = _representationBuilder.BuildRequestRepresentation(request);
var secret = ComputeHash(ApiKey, new SHA1CryptoServiceProvider());
string signature = _signatureCalculator.Signature(secret,
representation);
var header = new AuthenticationHeaderValue(Configuration.AuthenticationScheme, signature);
request.Headers.Authorization = header;
return base.SendAsync(request, cancellationToken);
}
private string ComputeHash(string inputData, HashAlgorithm algorithm)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(inputData);
byte[] hashed = algorithm.ComputeHash(inputBytes);
return Convert.ToBase64String(hashed);
}
}
public class RequestContentMd5Handler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
if (request.Content == null)
{
return await base.SendAsync(request, cancellationToken);
}
byte[] content = await request.Content.ReadAsByteArrayAsync();
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(content);
request.Content.Headers.ContentMD5 = hash;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
public class Configuration
{
public const string ClientIdHeader = "X-ApiAuth-ClientId";
public const string AuthenticationScheme = "ApiAuth";
public const int ValidityPeriodInMinutes = 5;
}
}
Below is a method you can use to obtain an HttpClient instance using the above delegating handlers:
private static HttpClient GetHttpClientWithHmac(int clientId, string apiKey)
{
// Create the delegating handlers
var delegHandler1 = new RequestContentMd5Handler();
var canonicalBuilder = new CanonicalRepresentationBuilder();
var hmacSignartureCalculator = new HmacSignatureCalculator();
var delegHandler2 = new HmacSigningHandler(canonicalBuilder, hmacSignartureCalculator);
delegHandler2.ClientId = clientId.ToString();
delegHandler2.ApiKey = apiKey;
delegHandler1.InnerHandler = delegHandler2;
var client = new HttpClient(delegHandler1);
return client;
}
private static HttpClient GetHttpClientWithHmac(int clientId, string clientApiKey)
{
// Create the delegating handlers
var delegHandler1 = new RequestContentMd5Handler();
var canonicalBuilder = new CanonicalRepresentationBuilder();
var hmacSignartureCalculator = new HmacSignatureCalculator();
var delegHandler2 = new HmacSigningHandler(canonicalBuilder, hmacSignartureCalculator);
delegHandler2.ClientId = clientId.ToString();
delegHandler2.ApiKey = clientApiKey;
delegHandler1.InnerHandler = delegHandler2;
var client = new HttpClient(delegHandler1);
return client;
}
Trigger new Test Run
Here is how you would use that in C# to trigger a new test run:
// Create the HttpClient object
var client = GetHttpClientWithHmac(clientId, apiKey);
// Trigger a new Test Run
var requestBody =
"{ 'Id': 2, 'BrowsersForTestrun': [{'BrowserType':'Chrome', 'Version':'55'}], 'EnvironmentId':1 }";
var content = new StringContent(requestBody, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://localhost:4443/api/testrun", content);
// Create the HttpClient object
var client = GetHttpClientWithHmac(clientId, apiKey);
// Trigger a new Test Run
var requestBody =
"{ 'Id': 2, 'BrowsersForTestrun': [{'BrowserType':'Chrome', 'Version':'55'}], 'EnvironmentId':1 }";
var content = new StringContent(requestBody, System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://localhost:4443/api/testrun", content);
The 'Id' passed in the request body corresponds to the id of the Test Run Configuration that you wish to trigger.
See the API Help section for more details on the parameters for each method.
Here is how you would trigger a new test run in PowerShell:
$Source = @"
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace infrastructure
{
public interface IBuildMessageRepresentation
{
string BuildRequestRepresentation(HttpRequestMessage requestMessage);
}
public class CanonicalRepresentationBuilder : IBuildMessageRepresentation
{
... rest of the code ommitted for brevity
}
... rest of the code ommitted for brevity
}
"@
if (-not ("infrastructure.CanonicalRepresentationBuilder" -as [type])) {
$Assem = (
"System.Net.Http"
)
Add-Type -TypeDefinition $Source -Language CSharp -ReferencedAssemblies $Assem
}
# initialize these three variables with actual values
$nugridiumEndpoint = "https://localhost:4443/api/testrun"
$clientId = 1
$sharedSecret = 'ajv1Twzq8jh17b84VcPN05BQEuhIc+2/4b7aaVphFOM='
# Create the delegating handlers
$delegHandler1 = new-object -TypeName infrastructure.RequestContentMd5Handler
$canonicalBuilder = new-object infrastructure.CanonicalRepresentationBuilder
$hmacSignartureCalculator = new-object infrastructure.HmacSignatureCalculator
$delegHandler2 = new-object infrastructure.HmacSigningHandler($canonicalBuilder, $hmacSignartureCalculator)
$delegHandler2.ClientId = $clientId
$delegHandler2.ApiKey = $sharedSecret
$delegHandler1.InnerHandler = $delegHandler2
# Create the HttpClient object
$client = New-Object -TypeName System.Net.Http.Httpclient($delegHandler1)
# Trigger a new Test Run (pass the test run config id, an array of browsers and an environment id)
$request = "{ 'Id': 2, 'BrowsersForTestrun': [{'BrowserType':'Chrome', 'Version':'55'}], 'EnvironmentId':1 }"
$content = new-object System.Net.Http.StringContent($request, [System.Text.Encoding]::UTF8, "application/json")
$task = $client.PostAsync($nugridiumEndpoint, $content)
$task.wait()
$Source = @"
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace infrastructure
{
public interface IBuildMessageRepresentation
{
string BuildRequestRepresentation(HttpRequestMessage requestMessage);
}
public class CanonicalRepresentationBuilder : IBuildMessageRepresentation
{
public string BuildRequestRepresentation(HttpRequestMessage requestMessage)
{
bool valid = IsRequestValid(requestMessage);
if (!valid)
{
return null;
}
if (!requestMessage.Headers.Date.HasValue)
{
return null;
}
DateTime date = requestMessage.Headers.Date.Value.UtcDateTime;
string md5 = requestMessage.Content == null ||
requestMessage.Content.Headers.ContentMD5 == null ? ""
: Convert.ToBase64String(requestMessage.Content.Headers.ContentMD5);
string httpMethod = requestMessage.Method.Method;
if (!requestMessage.Headers.Contains(Configuration.ClientIdHeader))
{
return null;
}
string clientId = requestMessage.Headers
.GetValues(Configuration.ClientIdHeader).First();
string uri = requestMessage.RequestUri.AbsolutePath.ToLower();
string representation = String.Join("\n", httpMethod,
md5, date.ToString(CultureInfo.InvariantCulture),
clientId, uri);
return representation;
}
private bool IsRequestValid(HttpRequestMessage requestMessage)
{
//for simplicity omitting headers check
return true;
}
}
public interface ICalculteSignature
{
string Signature(string secret, string value);
}
public class HmacSignatureCalculator : ICalculteSignature
{
public string Signature(string secret, string value)
{
var secretBytes = Encoding.UTF8.GetBytes(secret);
var valueBytes = Encoding.UTF8.GetBytes(value);
string signature;
using (var hmac = new HMACSHA256(secretBytes))
{
var hash = hmac.ComputeHash(valueBytes);
signature = Convert.ToBase64String(hash);
}
return signature;
}
}
public class HmacSigningHandler : HttpClientHandler
{
private readonly IBuildMessageRepresentation _representationBuilder;
private readonly ICalculteSignature _signatureCalculator;
public string ClientId { get; set; }
public string ApiKey { get; set; }
public HmacSigningHandler(
IBuildMessageRepresentation representationBuilder,
ICalculteSignature signatureCalculator)
{
_representationBuilder = representationBuilder;
_signatureCalculator = signatureCalculator;
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, sslPolicyErrors) => true;
}
protected override Task SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
{
if (!request.Headers.Contains(Configuration.ClientIdHeader))
{
request.Headers.Add(Configuration.ClientIdHeader, ClientId);
}
request.Headers.Date = new DateTimeOffset(DateTime.Now, TimeZoneInfo.Local.GetUtcOffset(DateTime.Now));
var representation = _representationBuilder.BuildRequestRepresentation(request);
var secret = ComputeHash(ApiKey, new SHA1CryptoServiceProvider());
string signature = _signatureCalculator.Signature(secret,
representation);
var header = new AuthenticationHeaderValue(Configuration.AuthenticationScheme, signature);
request.Headers.Authorization = header;
return base.SendAsync(request, cancellationToken);
}
private string ComputeHash(string inputData, HashAlgorithm algorithm)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(inputData);
byte[] hashed = algorithm.ComputeHash(inputBytes);
return Convert.ToBase64String(hashed);
}
}
public class RequestContentMd5Handler : DelegatingHandler
{
protected async override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
if (request.Content == null)
{
return await base.SendAsync(request, cancellationToken);
}
byte[] content = await request.Content.ReadAsByteArrayAsync();
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(content);
request.Content.Headers.ContentMD5 = hash;
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
public class Configuration
{
public const string ClientIdHeader = "X-ApiAuth-ClientId";
public const string AuthenticationScheme = "ApiAuth";
public const int ValidityPeriodInMinutes = 5;
}
}
"@
if (-not ("infrastructure.CanonicalRepresentationBuilder" -as [type])) {
$Assem = (
"System.Net.Http"
)
Add-Type -TypeDefinition $Source -Language CSharp -ReferencedAssemblies $Assem
}
# initialize these three variables with actual values
$nugridiumEndpoint = "https://localhost:4443/api/testrun"
$clientId = 1
$sharedSecret = 'ajv1Twzq8jh17b84VcPN05BQEuhIc+2/4b7aaVphFOM='
# Create the delegating handlers
$delegHandler1 = new-object -TypeName infrastructure.RequestContentMd5Handler
$canonicalBuilder = new-object infrastructure.CanonicalRepresentationBuilder
$hmacSignartureCalculator = new-object infrastructure.HmacSignatureCalculator
$delegHandler2 = new-object infrastructure.HmacSigningHandler($canonicalBuilder, $hmacSignartureCalculator)
$delegHandler2.ClientId = $clientId
$delegHandler2.ApiKey = $sharedSecret
$delegHandler1.InnerHandler = $delegHandler2
# Create the HttpClient object
$client = New-Object -TypeName System.Net.Http.Httpclient($delegHandler1)
# Trigger a new Test Run (pass the test run config id, an array of browsers and an environment id)
$request = "{ 'Id': 2, 'BrowsersForTestrun': [{'BrowserType':'Chrome', 'Version':'55'}], 'EnvironmentId':1 }"
$content = new-object System.Net.Http.StringContent($request, [System.Text.Encoding]::UTF8, "application/json")
$task = $client.PostAsync($nugridiumEndpoint, $content)
$task.wait()
Check for test run status
After you trigger a new test run, you can check for its status
and wait for the completion of all the test methods in that test run.
Here is the C# code to check the status of a test run:
// Create the HttpClient object
var testRunId = 1;
var client = GetHttpClientWithHmac(clientId, apiKey);
// Get test run details
var response = await client.GetAsync("https://localhost:4443/api/testrun/" + testRunId);
// Create the HttpClient object
var testRunId = 1;
var client = GetHttpClientWithHmac(clientId, apiKey);
// Get test run details
var response = await client.GetAsync("https://localhost:4443/api/testrun/" + testRunId);
Here is the Powershell script to check the status of a test run:
$testrunId = 1
$uploadEndpoint = "https://localhost:4443/api/testrun/$testrunId/"
$client = New-Object -TypeName System.Net.Http.Httpclient($delegHandler1)
$task1 = $client.GetAsync($uploadEndpoint)
$task1.wait();
$task2 = $task1.Result.Content.ReadAsStringAsync()
$task2.wait();
$responseObj2 = $task2.Result | ConvertFrom-Json
$status = $responseObj2.status
echo "status $status"
# $status could be 'completed', 'inQueue' or 'inProgress'
if ($status -eq "completed"){
$result = $responseObj2.responseState
# $result could be 'success', 'failure', 'error' or 'inconclusive'
write-host "Testrun done! Status: $status, Result: $result"
ForEach($test in $responseObj2.tests){
write-host $test.name $test.resultState
}
}
$testrunId = 1
$uploadEndpoint = "https://localhost:4443/api/testrun/$testrunId/"
$client = New-Object -TypeName System.Net.Http.Httpclient($delegHandler1)
$task1 = $client.GetAsync($uploadEndpoint)
$task1.wait();
$task2 = $task1.Result.Content.ReadAsStringAsync()
$task2.wait();
$responseObj2 = $task2.Result | ConvertFrom-Json
$status = $responseObj2.status
echo "status $status"
# $status could be 'completed', 'inQueue' or 'inProgress'
if ($status -eq "completed"){
$result = $responseObj2.responseState
# $result could be 'success', 'failure', 'error' or 'inconclusive'
write-host "Testrun done! Status: $status, Result: $result"
ForEach($test in $responseObj2.tests){
write-host $test.name $test.resultState
}
}