Skip to content

Commit 2dc1263

Browse files
author
Matthias Gessinger
committed
Add tests for reading deprecation policy
1 parent 765dcf1 commit 2dc1263

File tree

9 files changed

+297
-11
lines changed

9 files changed

+297
-11
lines changed

src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ISunsetPolicyBuilderExtensionsTest.cs renamed to src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IPolicyBuilderExtensionsTest.cs

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

33
namespace Asp.Versioning;
44

5-
public class ISunsetPolicyBuilderExtensionsTest
5+
public class IPolicyBuilderExtensionsTest
66
{
77
[Fact]
88
public void link_should_build_url_from_string()
@@ -11,23 +11,23 @@ public void link_should_build_url_from_string()
1111
var builder = Mock.Of<ISunsetPolicyBuilder>();
1212

1313
// act
14-
builder.Link( "http://tempuri.org" );
14+
builder.Link("http://tempuri.org");
1515

1616
// assert
17-
Mock.Get( builder ).Verify( b => b.Link( new Uri( "http://tempuri.org" ) ) );
17+
Mock.Get(builder).Verify(b => b.Link(new Uri("http://tempuri.org")));
1818
}
1919

2020
[Fact]
2121
public void effective_should_build_date_from_parts()
2222
{
2323
// arrange
2424
var builder = Mock.Of<ISunsetPolicyBuilder>();
25-
var date = new DateTime( 2022, 2, 1 );
25+
var date = new DateTime(2022, 2, 1);
2626

2727
// act
28-
builder.Effective( 2022, 2, 1 );
28+
builder.Effective(2022, 2, 1);
2929

3030
// assert
31-
Mock.Get( builder ).Verify( b => b.Effective( new( date ) ) );
31+
Mock.Get(builder).Verify(b => b.SetEffectiveDate(new(date)));
3232
}
3333
}

src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ protected virtual bool IsDeprecatedApi( HttpResponseMessage response )
6969
{
7070
ArgumentNullException.ThrowIfNull( response );
7171

72+
var deprecationPolicy = response.ReadDeprecationPolicy();
73+
74+
if ( deprecationPolicy.Date.HasValue && deprecationPolicy.Date <= DateTimeOffset.UtcNow )
75+
{
76+
return true;
77+
}
78+
7279
foreach ( var reportedApiVersion in enumerable.Deprecated( response, parser ) )
7380
{
7481
// don't use '==' operator because a derived type may not overload it

src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public ApiVersionEnumerator Supported(
4242
new( response, apiSupportedVersionsName, parser );
4343

4444
/// <summary>
45-
/// Creates and returns an enumerator for deprecated API versions.
45+
/// Creates and returns an enumerator for deprecated API versions, as read from the api-deprecated-versions header.
4646
/// </summary>
4747
/// <param name="response">The <see cref="HttpResponseMessage">HTTP response</see> to evaluate.</param>
4848
/// <param name="parser">The optional <see cref="IApiVersionParser">API version parser</see>.</param>

src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<Compile Include="$(BackportDir)CallerArgumentExpressionAttribute.cs" Visible="false" />
3434
<Compile Include="$(BackportDir)NullableAttributes.cs" Visible="false" />
3535
<Compile Include="$(BackportDir)StringExtensions.cs" Visible="false" />
36+
<Compile Include="$(BackportDir)DateTimeOffsetExtensions.cs" Visible="false" />
3637
</ItemGroup>
3738

3839
<ItemGroup>

src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ public static SunsetPolicy ReadSunsetPolicy( this HttpResponseMessage response )
7474
return policy;
7575
}
7676

77+
/// <summary>
78+
/// Formats the <paramref name="deprecationDate"/> as required for a Deprecation header.
79+
/// </summary>
80+
/// <param name="deprecationDate">The date when the api is deprecated.</param>
81+
/// <returns>A formatted string as required for a Deprecation header.</returns>
82+
public static string ToDeprecationHeaderValue( this DateTimeOffset deprecationDate )
83+
{
84+
var unixTimestamp = deprecationDate.ToUnixTimeSeconds();
85+
return unixTimestamp.ToString( "'@'0", CultureInfo.CurrentCulture );
86+
}
87+
7788
/// <summary>
7889
/// Gets an API deprecation policy from the HTTP response.
7990
/// </summary>
@@ -90,6 +101,7 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage
90101
if ( headers.TryGetValues( Deprecation, out var values ) )
91102
{
92103
var culture = CultureInfo.CurrentCulture;
104+
var style = NumberStyles.Integer;
93105

94106
foreach ( var value in values )
95107
{
@@ -99,9 +111,9 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage
99111
}
100112

101113
#if NETSTANDARD
102-
if ( long.TryParse( value.Substring( 1 ), out var unixTimestamp ) )
114+
if ( long.TryParse( value.Substring( 1 ), style, culture, out var unixTimestamp ) )
103115
#else
104-
if ( long.TryParse( value.AsSpan()[1..], out var unixTimestamp ) )
116+
if ( long.TryParse( value.AsSpan()[1..], style, culture, out var unixTimestamp ) )
105117
#endif
106118
{
107119
DateTimeOffset parsed;
@@ -111,7 +123,7 @@ public static DeprecationPolicy ReadDeprecationPolicy( this HttpResponseMessage
111123
parsed = DateTimeOffset.FromUnixTimeSeconds( unixTimestamp );
112124
#endif
113125

114-
if ( date == default || date < parsed )
126+
if ( date == default || date > parsed )
115127
{
116128
date = parsed;
117129
}

src/Client/test/Asp.Versioning.Http.Client.Tests/ApiVersionHandlerTest.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,31 @@ public async Task send_async_should_write_api_version_to_request()
2525
}
2626

2727
[Fact]
28-
public async Task send_async_should_signal_deprecated_api_version()
28+
public async Task send_async_should_not_notify_when_no_headers_are_set()
29+
{
30+
// arrange
31+
var writer = Mock.Of<IApiVersionWriter>();
32+
var notification = Mock.Of<IApiNotification>();
33+
var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" );
34+
var version = new ApiVersion( 1.0 );
35+
using var handler = new ApiVersionHandler( writer, version, notification )
36+
{
37+
InnerHandler = new TestServer(),
38+
};
39+
using var invoker = new HttpMessageInvoker( handler );
40+
41+
// act
42+
await invoker.SendAsync( request, default );
43+
44+
// assert
45+
Mock.Get( notification )
46+
.Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), It.IsAny<CancellationToken>() ), Times.Never );
47+
Mock.Get( notification )
48+
.Verify( n => n.OnNewApiAvailableAsync( It.IsAny<ApiNotificationContext>(), It.IsAny<CancellationToken>() ), Times.Never );
49+
}
50+
51+
[Fact]
52+
public async Task send_async_should_signal_deprecated_api_versions_from_header()
2953
{
3054
// arrange
3155
var writer = Mock.Of<IApiVersionWriter>();
@@ -50,6 +74,32 @@ public async Task send_async_should_signal_deprecated_api_version()
5074
.Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), default ) );
5175
}
5276

77+
[Fact]
78+
public async Task send_async_should_signal_deprecated_api_versions_from_deprecation_policy()
79+
{
80+
// arrange
81+
var writer = Mock.Of<IApiVersionWriter>();
82+
var notification = Mock.Of<IApiNotification>();
83+
var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" );
84+
var response = new HttpResponseMessage();
85+
var version = new ApiVersion( 1.0 );
86+
using var handler = new ApiVersionHandler( writer, version, notification )
87+
{
88+
InnerHandler = new TestServer( response ),
89+
};
90+
using var invoker = new HttpMessageInvoker( handler );
91+
92+
response.Headers.Add( "api-supported-versions", "2.0" );
93+
response.Headers.Add( "Deprecation", DateTimeOffset.UtcNow.ToDeprecationHeaderValue() );
94+
95+
// act
96+
await invoker.SendAsync( request, default );
97+
98+
// assert
99+
Mock.Get( notification )
100+
.Verify( n => n.OnApiDeprecatedAsync( It.IsAny<ApiNotificationContext>(), default ) );
101+
}
102+
53103
[Fact]
54104
public async Task send_async_should_signal_new_api_version()
55105
{

src/Client/test/Asp.Versioning.Http.Client.Tests/System.Net.Http/HttpResponseMessageExtensionsTest.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,87 @@ public void read_sunset_policy_should_ignore_unrelated_links()
8787
} );
8888
}
8989

90+
[Fact]
91+
public void read_deprecation_policy_should_parse_response()
92+
{
93+
// arrange
94+
var date = DateTimeOffset.Now;
95+
var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" );
96+
var response = new HttpResponseMessage() { RequestMessage = request };
97+
98+
response.Headers.Add( "deprecation", date.ToDeprecationHeaderValue() );
99+
response.Headers.Add( "link", "<policy>; rel=\"deprecation\"; type=\"text/html\"" );
100+
101+
// act
102+
var policy = response.ReadDeprecationPolicy();
103+
104+
// assert
105+
policy.Date.Value.ToLocalTime().Should().BeCloseTo( date, TimeSpan.FromSeconds( 2d ) );
106+
policy.Links.Single().Should().BeEquivalentTo(
107+
new LinkHeaderValue(
108+
new Uri( "http://tempuri.org/policy" ),
109+
"deprecation" )
110+
{
111+
Type = "text/html",
112+
} );
113+
}
114+
115+
[Fact]
116+
public void read_deprecation_policy_should_use_smallest_date()
117+
{
118+
// arrange
119+
var date = DateTimeOffset.Now;
120+
var expected = date.Subtract( TimeSpan.FromDays( 14 ) );
121+
var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" );
122+
var response = new HttpResponseMessage() { RequestMessage = request };
123+
124+
response.Headers.Add(
125+
"deprecation",
126+
new string[]
127+
{
128+
date.ToDeprecationHeaderValue(),
129+
expected.ToDeprecationHeaderValue(),
130+
expected.AddDays( 3 ).ToDeprecationHeaderValue(),
131+
} );
132+
133+
// act
134+
var policy = response.ReadDeprecationPolicy();
135+
136+
// assert
137+
policy.Date.Value.ToLocalTime().Should().BeCloseTo( expected, TimeSpan.FromSeconds( 2d ) );
138+
policy.HasLinks.Should().BeFalse();
139+
}
140+
141+
[Fact]
142+
public void read_deprecation_policy_should_ignore_unrelated_links()
143+
{
144+
// arrange
145+
var request = new HttpRequestMessage( HttpMethod.Get, "http://tempuri.org" );
146+
var response = new HttpResponseMessage() { RequestMessage = request };
147+
148+
response.Headers.Add(
149+
"link",
150+
new[]
151+
{
152+
"<swagger.json>; rel=\"openapi\"; type=\"application/json\" title=\"OpenAPI\"",
153+
"<policy>; rel=\"deprecation\"; type=\"text/html\"",
154+
"<docs>; rel=\"info\"; type=\"text/html\" title=\"Documentation\"",
155+
} );
156+
157+
// act
158+
var policy = response.ReadDeprecationPolicy();
159+
160+
// assert
161+
policy.Date.Should().BeNull();
162+
policy.Links.Single().Should().BeEquivalentTo(
163+
new LinkHeaderValue(
164+
new Uri( "http://tempuri.org/policy" ),
165+
"deprecation" )
166+
{
167+
Type = "text/html",
168+
} );
169+
}
170+
90171
[Fact]
91172
public void get_open_api_document_urls_should_return_expected_values()
92173
{
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning;
4+
5+
public class DeprecationPolicyBuilderTest
6+
{
7+
[Fact]
8+
public void constructor_should_not_allow_empty_name_and_version()
9+
{
10+
// arrange
11+
12+
13+
// act
14+
Func<DeprecationPolicyBuilder> @new = () => new DeprecationPolicyBuilder( default, default );
15+
16+
// assert
17+
@new.Should().Throw<ArgumentException>().And
18+
.Message.Should().Be( "'name' and 'apiVersion' cannot both be null." );
19+
}
20+
21+
[Fact]
22+
public void per_should_set_existing_immutable_policy()
23+
{
24+
// arrange
25+
var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default );
26+
var policy = new DeprecationPolicy();
27+
28+
// act
29+
builder.Per( policy );
30+
builder.Link( "http://tempuri.org" );
31+
32+
var result = builder.Build();
33+
34+
// assert
35+
result.Should().BeSameAs( policy );
36+
policy.HasLinks.Should().BeFalse();
37+
}
38+
39+
[Fact]
40+
public void link_should_should_return_existing_builder()
41+
{
42+
// arrange
43+
var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default );
44+
var expected = builder.Link( "http://tempuri.org" );
45+
46+
// act
47+
var result = builder.Link( "http://tempuri.org" );
48+
49+
// assert
50+
result.Should().BeSameAs( expected );
51+
}
52+
53+
[Fact]
54+
public void build_should_construct_sunset_policy()
55+
{
56+
// arrange
57+
var builder = new DeprecationPolicyBuilder( default, ApiVersion.Default );
58+
59+
builder.Effective( 2022, 2, 1 )
60+
.Link( "http://tempuri.org" );
61+
62+
// act
63+
var policy = builder.Build();
64+
65+
// assert
66+
policy.Should().BeEquivalentTo(
67+
new DeprecationPolicy(
68+
new DateTimeOffset( new DateTime( 2022, 2, 1 ) ),
69+
new LinkHeaderValue( new Uri( "http://tempuri.org" ), "sunset" ) ) );
70+
}
71+
}

0 commit comments

Comments
 (0)