|
8 | 8 | using ModelContextProtocol.Authentication; |
9 | 9 | using ModelContextProtocol.Client; |
10 | 10 | using System.Net; |
| 11 | +using System.Reflection; |
11 | 12 | using Xunit.Sdk; |
12 | 13 |
|
13 | 14 | namespace ModelContextProtocol.AspNetCore.Tests; |
@@ -188,6 +189,107 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() |
188 | 189 | transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); |
189 | 190 | } |
190 | 191 |
|
| 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 | + |
191 | 293 | private async Task<string?> HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) |
192 | 294 | { |
193 | 295 | var redirectResponse = await HttpClient.GetAsync(authorizationUrl, cancellationToken); |
|
0 commit comments