-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ensure build is shared across all test classes (#12)
- Loading branch information
Showing
9 changed files
with
346 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Newtonsoft.Json; | ||
using NUnit.Framework; | ||
using RichardSzalay.MockHttp; | ||
using SauceLabs.Visual.GraphQL; | ||
using SauceLabs.Visual.Models; | ||
|
||
namespace SauceLabs.Visual.Tests; | ||
|
||
public class BuildFactoryTest | ||
{ | ||
MockHttpMessageHandler MockedHandler; | ||
private const string _username = "dummy-username"; | ||
private const string _accessKey = "dummy-key"; | ||
|
||
[SetUp] | ||
public void Setup() | ||
{ | ||
MockedHandler = new MockHttpMessageHandler(); | ||
|
||
var createHandler = () => | ||
{ | ||
var id = RandomString(32); | ||
var content = new VisualBuild(id, $"http://dummy/test/{id}", BuildMode.Running); | ||
var resp = new HttpResponseMessage(HttpStatusCode.OK); | ||
resp.Content = new ReadOnlyMemoryContent(ToResult(content)); | ||
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); | ||
return Task.FromResult(resp); | ||
}; | ||
|
||
var base64EncodedAuthenticationString = | ||
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_username}:{_accessKey}")); | ||
var regions = new[] { Region.Staging, Region.UsEast4, Region.EuCentral1, Region.UsWest1 }; | ||
foreach (var r in regions) | ||
{ | ||
MockedHandler | ||
.When(r.Value.ToString()) | ||
.WithHeaders($"Authorization: Basic {base64EncodedAuthenticationString}") | ||
.WithPartialContent($"\"operationName\":\"{CreateBuildMutation.OperationName}\"") | ||
.Respond(createHandler); | ||
MockedHandler | ||
.When(r.Value.ToString()) | ||
.WithHeaders($"Authorization: Basic {base64EncodedAuthenticationString}") | ||
.WithPartialContent($"\"operationName\":\"{FinishBuildMutation.OperationName}\"") | ||
.Respond(createHandler); | ||
} | ||
MockedHandler.Fallback.Throw(new InvalidOperationException("No matching mock handler")); | ||
} | ||
|
||
[TearDown] | ||
public void Cleanup() | ||
{ | ||
MockedHandler.Dispose(); | ||
} | ||
|
||
private string RandomString(int length) | ||
{ | ||
const string chars = "abcdef0123456789"; | ||
return new string(Enumerable.Repeat(chars, length).Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); | ||
} | ||
|
||
private byte[] ToResult(object o) | ||
{ | ||
var nestedContent = new | ||
{ | ||
Data = new | ||
{ | ||
Result = o | ||
}, | ||
}; | ||
return Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(nestedContent)); | ||
} | ||
|
||
[Test] | ||
public async Task BuildFactory_ReturnSameBuildWhenSameRegion() | ||
{ | ||
var api = new VisualApi(Region.Staging, _username, _accessKey, MockedHandler.ToHttpClient()); | ||
var build1 = await BuildFactory.Get(api, new CreateBuildOptions()); | ||
var build2 = await BuildFactory.Get(api, new CreateBuildOptions()); | ||
Assert.AreEqual(build1, build2); | ||
} | ||
|
||
[Test] | ||
public async Task BuildFactory_ReturnDifferentBuildWhenDifferentRegion() | ||
{ | ||
var api1 = new VisualApi(Region.Staging, _username, _accessKey, MockedHandler.ToHttpClient()); | ||
var api2 = new VisualApi(Region.UsEast4, _username, _accessKey, MockedHandler.ToHttpClient()); | ||
var build1 = await BuildFactory.Get(api1, new CreateBuildOptions()); | ||
var build2 = await BuildFactory.Get(api2, new CreateBuildOptions()); | ||
Assert.AreNotEqual(build1, build2); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
namespace SauceLabs.Visual | ||
{ | ||
public class ApiBuildPair | ||
{ | ||
internal VisualBuild Build { get; } | ||
internal VisualApi Api { get; } | ||
|
||
internal ApiBuildPair(VisualApi api, VisualBuild build) | ||
{ | ||
Build = build; | ||
Api = api; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using SauceLabs.Visual.GraphQL; | ||
using SauceLabs.Visual.Utils; | ||
|
||
namespace SauceLabs.Visual | ||
{ | ||
internal static class BuildFactory | ||
{ | ||
private static readonly Dictionary<string, ApiBuildPair> Builds = new Dictionary<string, ApiBuildPair>(); | ||
|
||
/// <summary> | ||
/// <c>Get</c> returns the build matching with the requested region. | ||
/// If none is available, it returns a newly created build with <c>options</c>. | ||
/// It will also clone the input <c>api</c> to be able to close the build later. | ||
/// </summary> | ||
/// <param name="api">the api to use to create build</param> | ||
/// <param name="options">the options to use when creating the build</param> | ||
/// <returns></returns> | ||
internal static async Task<VisualBuild> Get(VisualApi api, CreateBuildOptions options) | ||
{ | ||
// Check if there is already a build for the current region. | ||
if (Builds.TryGetValue(api.Region.Name, out var build)) | ||
{ | ||
return build.Build; | ||
} | ||
|
||
var createdBuild = await Create(api, options); | ||
Builds[api.Region.Name] = new ApiBuildPair(api.Clone(), createdBuild); | ||
return createdBuild; | ||
} | ||
|
||
/// <summary> | ||
/// <c>FindRegionByBuild</c> returns the region matching the passed build. | ||
/// </summary> | ||
/// <param name="build"></param> | ||
/// <returns>the matching region name</returns> | ||
private static string? FindRegionByBuild(VisualBuild build) | ||
{ | ||
return Builds.Where(n => n.Value.Build == build).Select(n => n.Key).FirstOrDefault(); | ||
} | ||
|
||
/// <summary> | ||
/// <c>Close</c> finishes and forget about <c>build</c> | ||
/// </summary> | ||
/// <param name="build">the build to finish</param> | ||
internal static async Task Close(VisualBuild build) | ||
{ | ||
var key = FindRegionByBuild(build); | ||
if (key != null) | ||
{ | ||
await Close(key, Builds[key]); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// <c>Close</c> finishes and forget about <c>build</c> | ||
/// </summary> | ||
/// <param name="region">the build to finish</param> | ||
/// <param name="entry">the api/build pair</param> | ||
private static async Task Close(string region, ApiBuildPair entry) | ||
{ | ||
if (!entry.Build.IsExternal) | ||
{ | ||
await entry.Api.FinishBuild(entry.Build.Id); | ||
} | ||
Builds.Remove(region); | ||
entry.Api.Dispose(); | ||
} | ||
|
||
/// <summary> | ||
/// <c>CloseBuilds</c> closes all build that are still open. | ||
/// </summary> | ||
internal static async Task CloseBuilds() | ||
{ | ||
var regions = Builds.Keys; | ||
foreach (var region in regions) | ||
{ | ||
await Close(region, Builds[region]); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// <c>FindBuildById</c> returns the build identified by <c>buildId</c> | ||
/// </summary> | ||
/// <param name="api">a <c>VisualApi</c> object</param> | ||
/// <param name="buildId">the <c>buildId</c> to look for</param> | ||
/// <returns>the matching build</returns> | ||
/// <exception cref="VisualClientException">when build is not existing or has an invalid state</exception> | ||
private static async Task<VisualBuild> FindBuildById(VisualApi api, string buildId) | ||
{ | ||
try | ||
{ | ||
var build = (await api.Build(buildId)).EnsureValidResponse().Result; | ||
return new VisualBuild(build.Id, build.Url, build.Mode); | ||
} | ||
catch (VisualClientException) | ||
{ | ||
throw new VisualClientException($@"build {buildId} was not found"); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// <c>FindBuildById</c> returns the build identified by <c>buildId</c> | ||
/// </summary> | ||
/// <param name="api">a <c>VisualApi</c> object</param> | ||
/// <param name="customId">the <c>customId</c> to look for</param> | ||
/// <returns>the matching build</returns> | ||
/// <exception cref="VisualClientException">when build is not existing or has an invalid state</exception> | ||
private static async Task<VisualBuild?> FindBuildByCustomId(VisualApi api, string customId) | ||
{ | ||
try | ||
{ | ||
var build = (await api.BuildByCustomId(customId)).EnsureValidResponse().Result; | ||
return new VisualBuild(build.Id, build.Url, build.Mode); | ||
} | ||
catch (VisualClientException) | ||
{ | ||
return null; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// <c>CreateBuild</c> creates a new Visual build. | ||
/// </summary> | ||
/// <param name="api">the api client used for the build creation</param> | ||
/// <param name="options">the options for the build creation</param> | ||
/// <returns>a <c>VisualBuild</c> instance</returns> | ||
private static async Task<VisualBuild> Create(VisualApi api, CreateBuildOptions options) | ||
{ | ||
var build = await GetEffectiveBuild(api, EnvVars.BuildId, EnvVars.CustomId); | ||
if (build != null) | ||
{ | ||
if (!build.IsRunning()) | ||
{ | ||
throw new VisualClientException($"build {build.Id} is not RUNNING"); | ||
} | ||
|
||
build.IsExternal = true; | ||
return build; | ||
} | ||
|
||
options.CustomId ??= EnvVars.CustomId; | ||
var result = (await api.CreateBuild(new CreateBuildIn | ||
{ | ||
Name = options.Name, | ||
Project = options.Project, | ||
Branch = options.Branch, | ||
CustomId = options.CustomId, | ||
DefaultBranch = options.DefaultBranch, | ||
})).EnsureValidResponse(); | ||
|
||
build = new VisualBuild(result.Result.Id, result.Result.Url, result.Result.Mode) | ||
{ | ||
IsExternal = false | ||
}; | ||
return build; | ||
} | ||
|
||
/// <summary> | ||
/// <c>GetEffectiveBuild</c> tries to find the build matching the criterion provided by the user. | ||
/// </summary> | ||
/// <param name="api"></param> | ||
/// <param name="buildId"></param> | ||
/// <param name="customId"></param> | ||
/// <returns></returns> | ||
private static async Task<VisualBuild?> GetEffectiveBuild(VisualApi api, string? buildId, string? customId) | ||
{ | ||
if (!StringUtils.IsNullOrEmpty(buildId)) | ||
{ | ||
return await FindBuildById(api, buildId!.Trim()); | ||
} | ||
|
||
if (!StringUtils.IsNullOrEmpty(customId)) | ||
{ | ||
return await FindBuildByCustomId(api, customId!.Trim()); | ||
} | ||
return null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.