Skip to content

Commit 53e179f

Browse files
authored
[fix] Recursion causing stack overflow when disposing client/service (#588)
- Fix recursion causing stack overflow when disposing client/service - Add unit tests to verify disposal does not cause stack overflow - Don't dispose of client via service due to shared reference (service can be disposed of via client)
1 parent 435251a commit 53e179f

15 files changed

+197
-86
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Next Release
44

55
- Corrects all API documentation link references to point to their new locations
6+
- Fix recursion during disposal causing stack overflow
67

78
## v6.7.2 (2024-08-16)
89

EasyPost.Tests/ClientTest.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Linq;
32
using System.Net;
43
using System.Net.Http;
54
using System.Threading;
@@ -9,6 +8,7 @@
98
using EasyPost.Tests._Utilities;
109
using EasyPost.Tests._Utilities.Attributes;
1110
using Xunit;
11+
using CustomAssertions = EasyPost.Tests._Utilities.Assertions.Assert;
1212

1313
namespace EasyPost.Tests
1414
{
@@ -70,6 +70,13 @@ public void TestThreadSafety()
7070

7171
private const string FakeApikey = "fake_api_key";
7272

73+
[Fact]
74+
public void TestClientDisposal()
75+
{
76+
Client client = new(new ClientConfiguration(FakeApikey));
77+
CustomAssertions.DoesNotThrow(() => client.Dispose());
78+
}
79+
7380
[Fact]
7481
public void TestBaseUrlOverride()
7582
{

EasyPost.Tests/HttpTests/ClientConfigurationTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
using System;
22
using EasyPost.Tests._Utilities.Attributes;
33
using Xunit;
4+
using CustomAssertions = EasyPost.Tests._Utilities.Assertions.Assert;
45

56
namespace EasyPost.Tests.HttpTests
67
{
78
public class ClientConfigurationTest
89
{
910
#region Tests
1011

12+
[Fact]
13+
public void TestClientConfigurationDisposal()
14+
{
15+
ClientConfiguration configuration = new("not_a_real_api_key");
16+
CustomAssertions.DoesNotThrow(() => configuration.Dispose());
17+
}
18+
1119
[Fact]
1220
[Testing.Function]
1321
public void TestClientConfiguration()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Collections.Generic;
2+
using EasyPost._base;
3+
using EasyPost.Http;
4+
using Xunit;
5+
using CustomAssertions = EasyPost.Tests._Utilities.Assertions.Assert;
6+
7+
namespace EasyPost.Tests.HttpTests;
8+
9+
public class RequestTest
10+
{
11+
#region Tests
12+
13+
[Fact]
14+
public void TestRequestDisposal()
15+
{
16+
Request request = new("https://google.com", "not_a_real_endpoint", Method.Get, ApiVersion.V2, new Dictionary<string, object> { { "key", "value" } }, new Dictionary<string, string> { { "header_key", "header_value" } });
17+
CustomAssertions.DoesNotThrow(() => request.Dispose());
18+
}
19+
20+
#endregion
21+
}

EasyPost.Tests/ServicesTests/ServiceTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
using EasyPost.Exceptions.General;
66
using EasyPost.Http;
77
using EasyPost.Models.API;
8+
using EasyPost.Services;
89
using EasyPost.Tests._Utilities;
910
using EasyPost.Tests._Utilities.Attributes;
1011
using EasyPost.Utilities.Internal.Attributes;
1112
using Xunit;
13+
using CustomAssertions = EasyPost.Tests._Utilities.Assertions.Assert;
1214

1315
namespace EasyPost.Tests.ServicesTests
1416
{
@@ -21,6 +23,15 @@ public ServiceTests() : base("base_service")
2123
{
2224
}
2325

26+
[Fact]
27+
public void TestServiceDisposal()
28+
{
29+
Client client = new(new ClientConfiguration("not_a_real_api_key"));
30+
31+
AddressService addressService = client.Address;
32+
CustomAssertions.DoesNotThrow(() => addressService.Dispose());
33+
}
34+
2435
/// <summary>
2536
/// This test confirms that the GetNextPage method works as expected.
2637
/// This method is implemented on a per-service, but rather than testing in each service, we'll test it once here.

EasyPost.Tests/_Utilities/Assertions/AnyException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace EasyPost.Tests._Utilities.Assertions
44
{
55
/// <summary>
6-
/// Exception thrown when an Any assertion has one or more items fail an assertion.
6+
/// Exception thrown when an Any assertion has one or more items fail an assertion.
77
/// </summary>
88
public class AnyException : XunitException
99
{

EasyPost.Tests/_Utilities/Assertions/CollectionAsserts.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ namespace EasyPost.Tests._Utilities.Assertions
66
// ReSharper disable once PartialTypeWithSinglePart
77
public abstract partial class Assert
88
{
9-
private static void GuardArgumentNotNull(string argName, object argValue)
10-
{
11-
if (argValue == null)
12-
throw new ArgumentNullException(argName);
13-
}
14-
159
/// <summary>
1610
/// Verifies that any items in the collection pass when executed against
1711
/// action.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
3+
namespace EasyPost.Tests._Utilities.Assertions
4+
{
5+
// ReSharper disable once PartialTypeWithSinglePart
6+
public abstract partial class Assert
7+
{
8+
/// <summary>
9+
/// Verifies that an action does not throw an exception.
10+
/// </summary>
11+
/// <param name="action">The action to test</param>
12+
/// <exception cref="DoesNotThrowException">Thrown when the action throws an exception</exception>
13+
public static void DoesNotThrow(Action action)
14+
{
15+
GuardArgumentNotNull(nameof(action), action);
16+
17+
try
18+
{
19+
action();
20+
}
21+
catch (Exception ex)
22+
{
23+
throw new DoesNotThrowException(ex);
24+
}
25+
}
26+
27+
/// <summary>
28+
/// Verifies that an action does not throw a specific exception.
29+
/// </summary>
30+
/// <param name="action">The action to test</param>
31+
/// <typeparam name="T">The type of the exception to not throw</typeparam>
32+
/// <exception cref="DoesNotThrowException">Thrown when the action throws an exception of type T</exception>
33+
public static void DoesNotThrow<T>(Action action) where T : Exception
34+
{
35+
GuardArgumentNotNull(nameof(action), action);
36+
37+
try
38+
{
39+
action();
40+
}
41+
catch (Exception ex)
42+
{
43+
if (ex.GetType() == typeof(T))
44+
{
45+
throw new DoesNotThrowException(ex);
46+
}
47+
}
48+
}
49+
}
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using Xunit.Sdk;
3+
4+
namespace EasyPost.Tests._Utilities.Assertions
5+
{
6+
/// <summary>
7+
/// Exception thrown when a DoesNotThrow assertion fails.
8+
/// </summary>
9+
public class DoesNotThrowException : XunitException
10+
{
11+
/// <summary>
12+
/// Creates a new instance of the <see cref="DoesNotThrowException"/> class.
13+
/// </summary>
14+
public DoesNotThrowException(Exception ex)
15+
: base("Assert.DoesNotThrow() Failure", ex)
16+
{
17+
}
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace EasyPost.Tests._Utilities.Assertions
4+
{
5+
// ReSharper disable once PartialTypeWithSinglePart
6+
public abstract partial class Assert
7+
{
8+
private static void GuardArgumentNotNull(string argName, object argValue)
9+
{
10+
if (argValue == null)
11+
throw new ArgumentNullException(argName);
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)