Skip to content

Commit ee2f76b

Browse files
committed
Add CloneResourceMetadataClonesAllProperties test
1 parent 8b36076 commit ee2f76b

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

src/ModelContextProtocol.AspNetCore/Authentication/McpAuthenticationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
131131
return base.HandleChallengeAsync(properties);
132132
}
133133

134-
internal ProtectedResourceMetadata? CloneResourceMetadata(ProtectedResourceMetadata? resourceMetadata)
134+
internal static ProtectedResourceMetadata? CloneResourceMetadata(ProtectedResourceMetadata? resourceMetadata)
135135
{
136136
if (resourceMetadata is null)
137137
{

tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using ModelContextProtocol.Authentication;
99
using ModelContextProtocol.Client;
1010
using System.Net;
11+
using System.Reflection;
1112
using Xunit.Sdk;
1213

1314
namespace ModelContextProtocol.AspNetCore.Tests;
@@ -188,6 +189,107 @@ public async Task CanAuthenticate_WithDynamicClientRegistration()
188189
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
189190
}
190191

192+
[Fact]
193+
public void CloneResourceMetadataClonesAllProperties()
194+
{
195+
var propertyNames = typeof(ProtectedResourceMetadata).GetProperties().Select(property => property.Name).ToList();
196+
197+
// Set metadata properties to non-default values to verify they're copied.
198+
var metadata = new ProtectedResourceMetadata
199+
{
200+
Resource = new Uri("https://example.com/resource"),
201+
AuthorizationServers = [new Uri("https://auth1.example.com"), new Uri("https://auth2.example.com")],
202+
BearerMethodsSupported = ["header", "body", "query"],
203+
ScopesSupported = ["read", "write", "admin"],
204+
JwksUri = new Uri("https://example.com/.well-known/jwks.json"),
205+
ResourceSigningAlgValuesSupported = ["RS256", "ES256"],
206+
ResourceName = "Test Resource",
207+
ResourceDocumentation = new Uri("https://docs.example.com"),
208+
ResourcePolicyUri = new Uri("https://example.com/policy"),
209+
ResourceTosUri = new Uri("https://example.com/terms"),
210+
TlsClientCertificateBoundAccessTokens = true,
211+
AuthorizationDetailsTypesSupported = ["payment_initiation", "account_information"],
212+
DpopSigningAlgValuesSupported = ["RS256", "PS256"],
213+
DpopBoundAccessTokensRequired = true
214+
};
215+
216+
// Use reflection to call the internal CloneResourceMetadata method
217+
var handlerType = typeof(McpAuthenticationHandler);
218+
var cloneMethod = handlerType.GetMethod("CloneResourceMetadata", BindingFlags.Static | BindingFlags.NonPublic);
219+
Assert.NotNull(cloneMethod);
220+
221+
var clonedMetadata = (ProtectedResourceMetadata?)cloneMethod.Invoke(null, [metadata]);
222+
Assert.NotNull(clonedMetadata);
223+
224+
// Ensure the cloned metadata is not the same instance
225+
Assert.NotSame(metadata, clonedMetadata);
226+
227+
// Verify Resource property
228+
Assert.Equal(metadata.Resource, clonedMetadata.Resource);
229+
Assert.True(propertyNames.Remove(nameof(metadata.Resource)));
230+
231+
// Verify AuthorizationServers list is cloned and contains the same values
232+
Assert.NotSame(metadata.AuthorizationServers, clonedMetadata.AuthorizationServers);
233+
Assert.Equal(metadata.AuthorizationServers, clonedMetadata.AuthorizationServers);
234+
Assert.True(propertyNames.Remove(nameof(metadata.AuthorizationServers)));
235+
236+
// Verify BearerMethodsSupported list is cloned and contains the same values
237+
Assert.NotSame(metadata.BearerMethodsSupported, clonedMetadata.BearerMethodsSupported);
238+
Assert.Equal(metadata.BearerMethodsSupported, clonedMetadata.BearerMethodsSupported);
239+
Assert.True(propertyNames.Remove(nameof(metadata.BearerMethodsSupported)));
240+
241+
// Verify ScopesSupported list is cloned and contains the same values
242+
Assert.NotSame(metadata.ScopesSupported, clonedMetadata.ScopesSupported);
243+
Assert.Equal(metadata.ScopesSupported, clonedMetadata.ScopesSupported);
244+
Assert.True(propertyNames.Remove(nameof(metadata.ScopesSupported)));
245+
246+
// Verify JwksUri property
247+
Assert.Equal(metadata.JwksUri, clonedMetadata.JwksUri);
248+
Assert.True(propertyNames.Remove(nameof(metadata.JwksUri)));
249+
250+
// Verify ResourceSigningAlgValuesSupported list is cloned (nullable list)
251+
Assert.NotSame(metadata.ResourceSigningAlgValuesSupported, clonedMetadata.ResourceSigningAlgValuesSupported);
252+
Assert.Equal(metadata.ResourceSigningAlgValuesSupported, clonedMetadata.ResourceSigningAlgValuesSupported);
253+
Assert.True(propertyNames.Remove(nameof(metadata.ResourceSigningAlgValuesSupported)));
254+
255+
// Verify ResourceName property
256+
Assert.Equal(metadata.ResourceName, clonedMetadata.ResourceName);
257+
Assert.True(propertyNames.Remove(nameof(metadata.ResourceName)));
258+
259+
// Verify ResourceDocumentation property
260+
Assert.Equal(metadata.ResourceDocumentation, clonedMetadata.ResourceDocumentation);
261+
Assert.True(propertyNames.Remove(nameof(metadata.ResourceDocumentation)));
262+
263+
// Verify ResourcePolicyUri property
264+
Assert.Equal(metadata.ResourcePolicyUri, clonedMetadata.ResourcePolicyUri);
265+
Assert.True(propertyNames.Remove(nameof(metadata.ResourcePolicyUri)));
266+
267+
// Verify ResourceTosUri property
268+
Assert.Equal(metadata.ResourceTosUri, clonedMetadata.ResourceTosUri);
269+
Assert.True(propertyNames.Remove(nameof(metadata.ResourceTosUri)));
270+
271+
// Verify TlsClientCertificateBoundAccessTokens property
272+
Assert.Equal(metadata.TlsClientCertificateBoundAccessTokens, clonedMetadata.TlsClientCertificateBoundAccessTokens);
273+
Assert.True(propertyNames.Remove(nameof(metadata.TlsClientCertificateBoundAccessTokens)));
274+
275+
// Verify AuthorizationDetailsTypesSupported list is cloned (nullable list)
276+
Assert.NotSame(metadata.AuthorizationDetailsTypesSupported, clonedMetadata.AuthorizationDetailsTypesSupported);
277+
Assert.Equal(metadata.AuthorizationDetailsTypesSupported, clonedMetadata.AuthorizationDetailsTypesSupported);
278+
Assert.True(propertyNames.Remove(nameof(metadata.AuthorizationDetailsTypesSupported)));
279+
280+
// Verify DpopSigningAlgValuesSupported list is cloned (nullable list)
281+
Assert.NotSame(metadata.DpopSigningAlgValuesSupported, clonedMetadata.DpopSigningAlgValuesSupported);
282+
Assert.Equal(metadata.DpopSigningAlgValuesSupported, clonedMetadata.DpopSigningAlgValuesSupported);
283+
Assert.True(propertyNames.Remove(nameof(metadata.DpopSigningAlgValuesSupported)));
284+
285+
// Verify DpopBoundAccessTokensRequired property
286+
Assert.Equal(metadata.DpopBoundAccessTokensRequired, clonedMetadata.DpopBoundAccessTokensRequired);
287+
Assert.True(propertyNames.Remove(nameof(metadata.DpopBoundAccessTokensRequired)));
288+
289+
// Ensure we've checked every property. When new properties get added, we'll have to update this test along with the CloneResourceMetadata implementation.
290+
Assert.Empty(propertyNames);
291+
}
292+
191293
private async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
192294
{
193295
var redirectResponse = await HttpClient.GetAsync(authorizationUrl, cancellationToken);

0 commit comments

Comments
 (0)