Skip to content

Commit

Permalink
[chore] Add integration test for sync/async support (#500)
Browse files Browse the repository at this point in the history
- Add integration test for sync/async support
- Add VCR, utilities to integration test
  • Loading branch information
nwithan8 authored Aug 7, 2023
1 parent c86d36f commit 9fe4b6a
Show file tree
Hide file tree
Showing 15 changed files with 1,370 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

EasyPost.Tests/cassettes/**/* -diff
EasyPost.Tests/cassettes/**/* linguist-generated
EasyPost.Integration/cassettes/**/* -diff
EasyPost.Integration/cassettes/**/* linguist-generated

5 changes: 4 additions & 1 deletion EasyPost.Integration/EasyPost.Integration.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFrameworks>net462;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand All @@ -12,6 +12,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="EasyVCR" Version="0.9.0"/>
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand All @@ -28,4 +30,5 @@
<ProjectReference Include="..\EasyPost\EasyPost.csproj" />
</ItemGroup>


</Project>
178 changes: 178 additions & 0 deletions EasyPost.Integration/Synchronous.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
using System.Web.Mvc;
using EasyPost.Integration.Utilities;
using EasyPost.Integration.Utilities.Attributes;
using EasyPost.Models.API;
using EasyPost.Parameters.Parcel;
using Xunit;

namespace EasyPost.Integration;

public class Synchronous
{
private Utils.VCR Vcr { get; } = new("synchronous", Utils.ApiKey.Test);

/// <summary>
/// Test that an end-user can run asynchronous code asynchronously
/// </summary>
[Fact, Testing.Run]
public async void TestUserCanRunAsyncCodeAsynchronously()
{
var client = Vcr.SetUpTest("async");

// create a parcel asynchronously
var parcel = await client.Parcel.Create(new Create
{
Height = 1,
Length = 1,
Width = 1,
Weight = 1,
});
Assert.NotNull(parcel);
Assert.IsType<Parcel>(parcel);

string parcelId = parcel.Id!;

// retrieve a parcel asynchronously
var retrievedParcel = await client.Parcel.Retrieve(parcelId);
Assert.NotNull(retrievedParcel);
Assert.IsType<Parcel>(retrievedParcel);
}

/// <summary>
/// Test that an end-user can run asynchronous code synchronously via .Result
/// </summary>
[Fact, Testing.Run]
public void TestUserCanRunAsyncCodeSynchronouslyViaResult()
{
var client = Vcr.SetUpTest("via_result");

// create a parcel via .Result
var parcel = client.Parcel.Create(new Create
{
Height = 1,
Length = 1,
Width = 1,
Weight = 1,
}).Result;
Assert.NotNull(parcel);
Assert.IsType<Parcel>(parcel);

string parcelId = parcel.Id!;

// retrieve a parcel via .Result
var retrievedParcel = client.Parcel.Retrieve(parcelId).Result;
Assert.NotNull(retrievedParcel);
Assert.IsType<Parcel>(retrievedParcel);
}

/// <summary>
/// Test that an end-user can run asynchronous code synchronously via .GetAwaiter().GetResult()
/// </summary>
[Fact, Testing.Run]
public void TestUserCanRunAsyncCodeSynchronouslyViaGetAwaiter()
{
var client = Vcr.SetUpTest("via_get_awaiter");

// create a parcel via GetAwaiter().GetResult()
var parcel = client.Parcel.Create(new Create
{
Height = 1,
Length = 1,
Width = 1,
Weight = 1,
}).GetAwaiter().GetResult();
Assert.NotNull(parcel);
Assert.IsType<Parcel>(parcel);

string parcelId = parcel.Id!;

// retrieve a parcel via GetAwaiter().GetResult()
var retrievedParcel = client.Parcel.Retrieve(parcelId).GetAwaiter().GetResult();
Assert.NotNull(retrievedParcel);
Assert.IsType<Parcel>(retrievedParcel);
}
}

#pragma warning disable CA3147 // Mark Verb Handlers With Validate Antiforgery Token
/// <summary>
/// Test that an end-user can run asynchronous code in System.Web.Mvc.Controller
/// </summary>
public class SynchronousMvcController : System.Web.Mvc.Controller
{
private Utils.VCR Vcr { get; } = new("synchronous_mvc_controller", Utils.ApiKey.Test);

/// <summary>
/// Test that an end-user can run asynchronous code asynchronously
/// </summary>
[Fact, Testing.Run]
public async Task<ActionResult> TestUserCanRunAsyncCodeAsynchronously()
{
var client = Vcr.SetUpTest("async");

// create a parcel asynchronously
var parcel = await client.Parcel.Create(new Create
{
Height = 1,
Length = 1,
Width = 1,
Weight = 1,
});
Assert.NotNull(parcel);
Assert.IsType<Parcel>(parcel);

string parcelId = parcel.Id!;

// retrieve a parcel asynchronously
var retrievedParcel = await client.Parcel.Retrieve(parcelId);
Assert.NotNull(retrievedParcel);
Assert.IsType<Parcel>(retrievedParcel);

return new EmptyResult();
}

/// <summary>
/// Test that an end-user can run asynchronous code synchronously via TaskFactory
/// Ref: https://gist.github.com/leonardochaia/98ce57bcee39c18d88682424a6ffe305
/// </summary>
[Fact, Testing.Run]
public ActionResult TestUserCanRunAsyncCodeSynchronouslyViaTaskFactory()
{
var client = Vcr.SetUpTest("via_task_factory");

// create a parcel via TaskFactory (via AsyncHelper)
var parcel = AsyncHelper.RunSync(() => client.Parcel.Create(new Create
{
Height = 1,
Length = 1,
Width = 1,
Weight = 1,
}));
Assert.NotNull(parcel);
Assert.IsType<Parcel>(parcel);

string parcelId = parcel.Id!;

// retrieve a parcel via TaskFactory (via AsyncHelper)
var retrievedParcel = AsyncHelper.RunSync(() => client.Parcel.Retrieve(parcelId));
Assert.NotNull(retrievedParcel);
Assert.IsType<Parcel>(retrievedParcel);

return new EmptyResult();
}

private static class AsyncHelper
{
private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

public static void RunSync(Func<Task> func)
{
TaskFactory.StartNew<Task>(func).Unwrap().GetAwaiter().GetResult();
}

public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return TaskFactory.StartNew<Task<TResult>>(func).Unwrap<TResult>().GetAwaiter().GetResult();
}
}
}
#pragma warning restore CA3147 // Mark Verb Handlers With Validate Antiforgery Token
174 changes: 174 additions & 0 deletions EasyPost.Integration/TestUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System.Runtime.CompilerServices;
using EasyVCR;

// ReSharper disable once CheckNamespace
namespace EasyPost.Integration.Utilities
{
public class Utils
{
internal const string ApiKeyFailedToPull = "couldnotpullapikey";

private static readonly List<string> BodyCensors = new()
{
"api_keys",
"children",
"client_ip",
"credentials",
"email",
"key",
"keys",
"phone_number",
"phone",
"test_credentials"
};

private static readonly List<string> HeaderCensors = new()
{
"Authorization",
"User-Agent"
};

private static readonly List<string> QueryCensors = new()
{
"card[number]",
"card[cvc]"
};

public enum ApiKey
{
Test,
Production,
Partner,
Referral,
Mock,
}

public static string GetSourceFileDirectory([CallerFilePath] string sourceFilePath = "") => Path.GetDirectoryName(sourceFilePath)!;

internal static string GetApiKey(ApiKey apiKey)
{
string keyName = apiKey switch
{
ApiKey.Test => "EASYPOST_TEST_API_KEY",
ApiKey.Production => "EASYPOST_PROD_API_KEY",
ApiKey.Partner => "PARTNER_USER_PROD_API_KEY",
ApiKey.Referral => "REFERRAL_CUSTOMER_PROD_API_KEY",
ApiKey.Mock => "EASYPOST_MOCK_API_KEY", // does not exist, will trigger to use ApiKeyFailedToPull
#pragma warning disable CA2201
var _ => throw new Exception(Constants.ErrorMessages.InvalidApiKeyType)
#pragma warning restore CA2201
};

return Environment.GetEnvironmentVariable(keyName) ?? ApiKeyFailedToPull; // if can't pull from environment, will use a fake key. Won't matter on replay.
}

// ReSharper disable once InconsistentNaming
internal static Client GetBasicVCRClient(string apiKey, HttpClient? vcrClient = null) => new(new ClientConfiguration(apiKey)
{
CustomHttpClient = vcrClient,
});

internal static string ReadFile(string path)
{
string filePath = Path.Combine(GetSourceFileDirectory(), path);
return File.ReadAllText(filePath);
}

internal static string NetVersion
{
get
{
string netVersion = "net";
#if NET462
netVersion = "netstandard";
#endif

return netVersion;
}
}

public class VCR
{
// Cassettes folder will always been in the same directory as this TestUtils.cs file
private const string CassettesFolder = "cassettes";

private readonly string _apiKey;

private readonly string _testCassettesFolder;

private readonly EasyVCR.VCR _vcr;

public VCR(string? testCassettesFolder = null, ApiKey apiKey = ApiKey.Test)
{
Censors censors = new("<REDACTED>");
censors.CensorHeadersByKeys(HeaderCensors);
censors.CensorQueryParametersByKeys(QueryCensors);
censors.CensorBodyElementsByKeys(BodyCensors);

AdvancedSettings advancedSettings = new()
{
MatchRules = MatchRules.DefaultStrict,
Censors = censors,
SimulateDelay = false,
ManualDelay = 0,
ValidTimeFrame = TimeFrame.Months6,
WhenExpired = ExpirationActions.Warn
};
_vcr = new EasyVCR.VCR(advancedSettings);

_apiKey = GetApiKey(apiKey);

_testCassettesFolder = Path.Combine(GetSourceFileDirectory(), CassettesFolder); // create "cassettes" folder in same directory as test files

string netVersionFolder = NetVersion;

_testCassettesFolder = Path.Combine(_testCassettesFolder, netVersionFolder); // create .NET version-specific folder in "cassettes" folder

if (testCassettesFolder != null)
{
_testCassettesFolder = Path.Combine(_testCassettesFolder, testCassettesFolder); // create test group folder in .NET version-specific folder
}

// if folder doesn't exist, create it
if (!Directory.Exists(_testCassettesFolder))
{
Directory.CreateDirectory(_testCassettesFolder);
}
}

internal bool IsRecording() => _vcr.Mode == Mode.Record;

internal Client SetUpTest(string cassetteName, Func<string, HttpClient, Client> getClientFunc, string? overrideApiKey = null)
{
// override api key if needed
string apiKey = overrideApiKey ?? _apiKey;

// set up cassette
Cassette cassette = new(_testCassettesFolder, cassetteName, new CassetteOrder.Alphabetical());

// add cassette to vcr
_vcr.Insert(cassette);

string filePath = Path.Combine(_testCassettesFolder, cassetteName + ".json");
if (!File.Exists(filePath))
{
// if cassette doesn't exist, switch to record mode
_vcr.Record();
}
else
{
// if cassette exists, switch to replay mode
_vcr.Replay();
}

// get EasyPost client
return getClientFunc(apiKey, _vcr.Client);
}

internal Client SetUpTest(string cassetteName, string? overrideApiKey = null)
{
return SetUpTest(cassetteName, GetBasicVCRClient, overrideApiKey);
}
}
}
}
8 changes: 8 additions & 0 deletions EasyPost.Integration/Utilities/Attributes/TestType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,13 @@ internal sealed class Access : BaseCustomAttribute
internal sealed class Compile : BaseCustomAttribute
{
}

/// <summary>
/// Marks an integration test that is testing run-time behavior (test that code as written will run)
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class Run : BaseCustomAttribute
{
}
}
}
Loading

0 comments on commit 9fe4b6a

Please sign in to comment.