Skip to content

Commit 9fe4b6a

Browse files
authored
[chore] Add integration test for sync/async support (#500)
- Add integration test for sync/async support - Add VCR, utilities to integration test
1 parent c86d36f commit 9fe4b6a

File tree

15 files changed

+1370
-1
lines changed

15 files changed

+1370
-1
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22

33
EasyPost.Tests/cassettes/**/* -diff
44
EasyPost.Tests/cassettes/**/* linguist-generated
5+
EasyPost.Integration/cassettes/**/* -diff
6+
EasyPost.Integration/cassettes/**/* linguist-generated
7+

EasyPost.Integration/EasyPost.Integration.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFrameworks>net462;netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>
55
<LangVersion>10</LangVersion>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
@@ -12,6 +12,8 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15+
<PackageReference Include="EasyVCR" Version="0.9.0"/>
16+
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9"/>
1517
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
1618
<PackageReference Include="xunit" Version="2.4.2" />
1719
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
@@ -28,4 +30,5 @@
2830
<ProjectReference Include="..\EasyPost\EasyPost.csproj" />
2931
</ItemGroup>
3032

33+
3134
</Project>

EasyPost.Integration/Synchronous.cs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
using System.Web.Mvc;
2+
using EasyPost.Integration.Utilities;
3+
using EasyPost.Integration.Utilities.Attributes;
4+
using EasyPost.Models.API;
5+
using EasyPost.Parameters.Parcel;
6+
using Xunit;
7+
8+
namespace EasyPost.Integration;
9+
10+
public class Synchronous
11+
{
12+
private Utils.VCR Vcr { get; } = new("synchronous", Utils.ApiKey.Test);
13+
14+
/// <summary>
15+
/// Test that an end-user can run asynchronous code asynchronously
16+
/// </summary>
17+
[Fact, Testing.Run]
18+
public async void TestUserCanRunAsyncCodeAsynchronously()
19+
{
20+
var client = Vcr.SetUpTest("async");
21+
22+
// create a parcel asynchronously
23+
var parcel = await client.Parcel.Create(new Create
24+
{
25+
Height = 1,
26+
Length = 1,
27+
Width = 1,
28+
Weight = 1,
29+
});
30+
Assert.NotNull(parcel);
31+
Assert.IsType<Parcel>(parcel);
32+
33+
string parcelId = parcel.Id!;
34+
35+
// retrieve a parcel asynchronously
36+
var retrievedParcel = await client.Parcel.Retrieve(parcelId);
37+
Assert.NotNull(retrievedParcel);
38+
Assert.IsType<Parcel>(retrievedParcel);
39+
}
40+
41+
/// <summary>
42+
/// Test that an end-user can run asynchronous code synchronously via .Result
43+
/// </summary>
44+
[Fact, Testing.Run]
45+
public void TestUserCanRunAsyncCodeSynchronouslyViaResult()
46+
{
47+
var client = Vcr.SetUpTest("via_result");
48+
49+
// create a parcel via .Result
50+
var parcel = client.Parcel.Create(new Create
51+
{
52+
Height = 1,
53+
Length = 1,
54+
Width = 1,
55+
Weight = 1,
56+
}).Result;
57+
Assert.NotNull(parcel);
58+
Assert.IsType<Parcel>(parcel);
59+
60+
string parcelId = parcel.Id!;
61+
62+
// retrieve a parcel via .Result
63+
var retrievedParcel = client.Parcel.Retrieve(parcelId).Result;
64+
Assert.NotNull(retrievedParcel);
65+
Assert.IsType<Parcel>(retrievedParcel);
66+
}
67+
68+
/// <summary>
69+
/// Test that an end-user can run asynchronous code synchronously via .GetAwaiter().GetResult()
70+
/// </summary>
71+
[Fact, Testing.Run]
72+
public void TestUserCanRunAsyncCodeSynchronouslyViaGetAwaiter()
73+
{
74+
var client = Vcr.SetUpTest("via_get_awaiter");
75+
76+
// create a parcel via GetAwaiter().GetResult()
77+
var parcel = client.Parcel.Create(new Create
78+
{
79+
Height = 1,
80+
Length = 1,
81+
Width = 1,
82+
Weight = 1,
83+
}).GetAwaiter().GetResult();
84+
Assert.NotNull(parcel);
85+
Assert.IsType<Parcel>(parcel);
86+
87+
string parcelId = parcel.Id!;
88+
89+
// retrieve a parcel via GetAwaiter().GetResult()
90+
var retrievedParcel = client.Parcel.Retrieve(parcelId).GetAwaiter().GetResult();
91+
Assert.NotNull(retrievedParcel);
92+
Assert.IsType<Parcel>(retrievedParcel);
93+
}
94+
}
95+
96+
#pragma warning disable CA3147 // Mark Verb Handlers With Validate Antiforgery Token
97+
/// <summary>
98+
/// Test that an end-user can run asynchronous code in System.Web.Mvc.Controller
99+
/// </summary>
100+
public class SynchronousMvcController : System.Web.Mvc.Controller
101+
{
102+
private Utils.VCR Vcr { get; } = new("synchronous_mvc_controller", Utils.ApiKey.Test);
103+
104+
/// <summary>
105+
/// Test that an end-user can run asynchronous code asynchronously
106+
/// </summary>
107+
[Fact, Testing.Run]
108+
public async Task<ActionResult> TestUserCanRunAsyncCodeAsynchronously()
109+
{
110+
var client = Vcr.SetUpTest("async");
111+
112+
// create a parcel asynchronously
113+
var parcel = await client.Parcel.Create(new Create
114+
{
115+
Height = 1,
116+
Length = 1,
117+
Width = 1,
118+
Weight = 1,
119+
});
120+
Assert.NotNull(parcel);
121+
Assert.IsType<Parcel>(parcel);
122+
123+
string parcelId = parcel.Id!;
124+
125+
// retrieve a parcel asynchronously
126+
var retrievedParcel = await client.Parcel.Retrieve(parcelId);
127+
Assert.NotNull(retrievedParcel);
128+
Assert.IsType<Parcel>(retrievedParcel);
129+
130+
return new EmptyResult();
131+
}
132+
133+
/// <summary>
134+
/// Test that an end-user can run asynchronous code synchronously via TaskFactory
135+
/// Ref: https://gist.github.com/leonardochaia/98ce57bcee39c18d88682424a6ffe305
136+
/// </summary>
137+
[Fact, Testing.Run]
138+
public ActionResult TestUserCanRunAsyncCodeSynchronouslyViaTaskFactory()
139+
{
140+
var client = Vcr.SetUpTest("via_task_factory");
141+
142+
// create a parcel via TaskFactory (via AsyncHelper)
143+
var parcel = AsyncHelper.RunSync(() => client.Parcel.Create(new Create
144+
{
145+
Height = 1,
146+
Length = 1,
147+
Width = 1,
148+
Weight = 1,
149+
}));
150+
Assert.NotNull(parcel);
151+
Assert.IsType<Parcel>(parcel);
152+
153+
string parcelId = parcel.Id!;
154+
155+
// retrieve a parcel via TaskFactory (via AsyncHelper)
156+
var retrievedParcel = AsyncHelper.RunSync(() => client.Parcel.Retrieve(parcelId));
157+
Assert.NotNull(retrievedParcel);
158+
Assert.IsType<Parcel>(retrievedParcel);
159+
160+
return new EmptyResult();
161+
}
162+
163+
private static class AsyncHelper
164+
{
165+
private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
166+
167+
public static void RunSync(Func<Task> func)
168+
{
169+
TaskFactory.StartNew<Task>(func).Unwrap().GetAwaiter().GetResult();
170+
}
171+
172+
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
173+
{
174+
return TaskFactory.StartNew<Task<TResult>>(func).Unwrap<TResult>().GetAwaiter().GetResult();
175+
}
176+
}
177+
}
178+
#pragma warning restore CA3147 // Mark Verb Handlers With Validate Antiforgery Token

EasyPost.Integration/TestUtils.cs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System.Runtime.CompilerServices;
2+
using EasyVCR;
3+
4+
// ReSharper disable once CheckNamespace
5+
namespace EasyPost.Integration.Utilities
6+
{
7+
public class Utils
8+
{
9+
internal const string ApiKeyFailedToPull = "couldnotpullapikey";
10+
11+
private static readonly List<string> BodyCensors = new()
12+
{
13+
"api_keys",
14+
"children",
15+
"client_ip",
16+
"credentials",
17+
"email",
18+
"key",
19+
"keys",
20+
"phone_number",
21+
"phone",
22+
"test_credentials"
23+
};
24+
25+
private static readonly List<string> HeaderCensors = new()
26+
{
27+
"Authorization",
28+
"User-Agent"
29+
};
30+
31+
private static readonly List<string> QueryCensors = new()
32+
{
33+
"card[number]",
34+
"card[cvc]"
35+
};
36+
37+
public enum ApiKey
38+
{
39+
Test,
40+
Production,
41+
Partner,
42+
Referral,
43+
Mock,
44+
}
45+
46+
public static string GetSourceFileDirectory([CallerFilePath] string sourceFilePath = "") => Path.GetDirectoryName(sourceFilePath)!;
47+
48+
internal static string GetApiKey(ApiKey apiKey)
49+
{
50+
string keyName = apiKey switch
51+
{
52+
ApiKey.Test => "EASYPOST_TEST_API_KEY",
53+
ApiKey.Production => "EASYPOST_PROD_API_KEY",
54+
ApiKey.Partner => "PARTNER_USER_PROD_API_KEY",
55+
ApiKey.Referral => "REFERRAL_CUSTOMER_PROD_API_KEY",
56+
ApiKey.Mock => "EASYPOST_MOCK_API_KEY", // does not exist, will trigger to use ApiKeyFailedToPull
57+
#pragma warning disable CA2201
58+
var _ => throw new Exception(Constants.ErrorMessages.InvalidApiKeyType)
59+
#pragma warning restore CA2201
60+
};
61+
62+
return Environment.GetEnvironmentVariable(keyName) ?? ApiKeyFailedToPull; // if can't pull from environment, will use a fake key. Won't matter on replay.
63+
}
64+
65+
// ReSharper disable once InconsistentNaming
66+
internal static Client GetBasicVCRClient(string apiKey, HttpClient? vcrClient = null) => new(new ClientConfiguration(apiKey)
67+
{
68+
CustomHttpClient = vcrClient,
69+
});
70+
71+
internal static string ReadFile(string path)
72+
{
73+
string filePath = Path.Combine(GetSourceFileDirectory(), path);
74+
return File.ReadAllText(filePath);
75+
}
76+
77+
internal static string NetVersion
78+
{
79+
get
80+
{
81+
string netVersion = "net";
82+
#if NET462
83+
netVersion = "netstandard";
84+
#endif
85+
86+
return netVersion;
87+
}
88+
}
89+
90+
public class VCR
91+
{
92+
// Cassettes folder will always been in the same directory as this TestUtils.cs file
93+
private const string CassettesFolder = "cassettes";
94+
95+
private readonly string _apiKey;
96+
97+
private readonly string _testCassettesFolder;
98+
99+
private readonly EasyVCR.VCR _vcr;
100+
101+
public VCR(string? testCassettesFolder = null, ApiKey apiKey = ApiKey.Test)
102+
{
103+
Censors censors = new("<REDACTED>");
104+
censors.CensorHeadersByKeys(HeaderCensors);
105+
censors.CensorQueryParametersByKeys(QueryCensors);
106+
censors.CensorBodyElementsByKeys(BodyCensors);
107+
108+
AdvancedSettings advancedSettings = new()
109+
{
110+
MatchRules = MatchRules.DefaultStrict,
111+
Censors = censors,
112+
SimulateDelay = false,
113+
ManualDelay = 0,
114+
ValidTimeFrame = TimeFrame.Months6,
115+
WhenExpired = ExpirationActions.Warn
116+
};
117+
_vcr = new EasyVCR.VCR(advancedSettings);
118+
119+
_apiKey = GetApiKey(apiKey);
120+
121+
_testCassettesFolder = Path.Combine(GetSourceFileDirectory(), CassettesFolder); // create "cassettes" folder in same directory as test files
122+
123+
string netVersionFolder = NetVersion;
124+
125+
_testCassettesFolder = Path.Combine(_testCassettesFolder, netVersionFolder); // create .NET version-specific folder in "cassettes" folder
126+
127+
if (testCassettesFolder != null)
128+
{
129+
_testCassettesFolder = Path.Combine(_testCassettesFolder, testCassettesFolder); // create test group folder in .NET version-specific folder
130+
}
131+
132+
// if folder doesn't exist, create it
133+
if (!Directory.Exists(_testCassettesFolder))
134+
{
135+
Directory.CreateDirectory(_testCassettesFolder);
136+
}
137+
}
138+
139+
internal bool IsRecording() => _vcr.Mode == Mode.Record;
140+
141+
internal Client SetUpTest(string cassetteName, Func<string, HttpClient, Client> getClientFunc, string? overrideApiKey = null)
142+
{
143+
// override api key if needed
144+
string apiKey = overrideApiKey ?? _apiKey;
145+
146+
// set up cassette
147+
Cassette cassette = new(_testCassettesFolder, cassetteName, new CassetteOrder.Alphabetical());
148+
149+
// add cassette to vcr
150+
_vcr.Insert(cassette);
151+
152+
string filePath = Path.Combine(_testCassettesFolder, cassetteName + ".json");
153+
if (!File.Exists(filePath))
154+
{
155+
// if cassette doesn't exist, switch to record mode
156+
_vcr.Record();
157+
}
158+
else
159+
{
160+
// if cassette exists, switch to replay mode
161+
_vcr.Replay();
162+
}
163+
164+
// get EasyPost client
165+
return getClientFunc(apiKey, _vcr.Client);
166+
}
167+
168+
internal Client SetUpTest(string cassetteName, string? overrideApiKey = null)
169+
{
170+
return SetUpTest(cassetteName, GetBasicVCRClient, overrideApiKey);
171+
}
172+
}
173+
}
174+
}

EasyPost.Integration/Utilities/Attributes/TestType.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,13 @@ internal sealed class Access : BaseCustomAttribute
2121
internal sealed class Compile : BaseCustomAttribute
2222
{
2323
}
24+
25+
/// <summary>
26+
/// Marks an integration test that is testing run-time behavior (test that code as written will run)
27+
/// </summary>
28+
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
29+
internal sealed class Run : BaseCustomAttribute
30+
{
31+
}
2432
}
2533
}

0 commit comments

Comments
 (0)