1111using System . Net . Http . Json ;
1212using System . Text . Encodings . Web ;
1313
14- namespace ModelContextProtocol . AspNetCore . Tests . Authentication ;
14+ namespace ModelContextProtocol . AspNetCore . Tests . OAuth ;
1515
1616public class McpAuthenticationHandlerTests ( ITestOutputHelper outputHelper ) : KestrelInMemoryTest ( outputHelper )
1717{
@@ -23,6 +23,7 @@ public async Task Challenge_WithRelativeResourceMetadataUri_SetsAbsoluteUrl()
2323 await using var app = await StartAuthenticationServerAsync ( options =>
2424 {
2525 options . ResourceMetadataUri = new Uri ( metadataPath , UriKind . Relative ) ;
26+ options . ResourceMetadata ! . Resource = new Uri ( "http://localhost:5000/challenge" ) ;
2627 } ) ;
2728
2829 using var challengeResponse = await HttpClient . GetAsync ( new Uri ( "/challenge" , UriKind . Relative ) , HttpCompletionOption . ResponseHeadersRead , TestContext . Current . CancellationToken ) ;
@@ -36,14 +37,34 @@ public async Task Challenge_WithRelativeResourceMetadataUri_SetsAbsoluteUrl()
3637 metadataResponse . EnsureSuccessStatusCode ( ) ;
3738 }
3839
40+ [ Fact ]
41+ public async Task MetadataRequest_CustomResourceMetadataUriWithoutResource_ThrowsInvalidOperationException ( )
42+ {
43+ const string metadataPath = "/.well-known/custom-metadata" ;
44+
45+ await using var app = await StartAuthenticationServerAsync ( options =>
46+ {
47+ options . ResourceMetadataUri = new Uri ( metadataPath , UriKind . Relative ) ;
48+ } ) ;
49+
50+ using var response = await HttpClient . GetAsync ( new Uri ( metadataPath , UriKind . Relative ) , TestContext . Current . CancellationToken ) ;
51+
52+ Assert . Equal ( HttpStatusCode . InternalServerError , response . StatusCode ) ;
53+ Assert . Contains ( MockLoggerProvider . LogMessages , log =>
54+ log . LogLevel == LogLevel . Error &&
55+ log . Exception is InvalidOperationException &&
56+ log . Exception . Message . Contains ( "ResourceMetadata.Resource could not be determined" , StringComparison . Ordinal ) ) ;
57+ }
58+
3959 [ Fact ]
4060 public async Task Challenge_WithAbsoluteResourceMetadataUri_SetsConfiguredUrl ( )
4161 {
42- var metadataUri = new Uri ( "http://localhost:5000/.well-known/custom-absolute" , UriKind . Absolute ) ;
62+ var metadataUri = new Uri ( "http://localhost:5000/.well-known/custom-absolute" ) ;
4363
4464 await using var app = await StartAuthenticationServerAsync ( options =>
4565 {
4666 options . ResourceMetadataUri = metadataUri ;
67+ options . ResourceMetadata ! . Resource = new Uri ( "http://localhost:5000/challenge" ) ;
4768 } ) ;
4869
4970 using var challengeResponse = await HttpClient . GetAsync ( new Uri ( "/challenge" , UriKind . Relative ) , HttpCompletionOption . ResponseHeadersRead , TestContext . Current . CancellationToken ) ;
@@ -60,7 +81,7 @@ public async Task Challenge_WithAbsoluteResourceMetadataUri_SetsConfiguredUrl()
6081 [ Fact ]
6182 public async Task MetadataRequest_WithHostMismatch_LogsWarning ( )
6283 {
63- var metadataUri = new Uri ( "http://expected-host:5000/.well-known/host-mismatch" , UriKind . Absolute ) ;
84+ var metadataUri = new Uri ( "http://expected-host:5000/.well-known/host-mismatch" ) ;
6485
6586 await using var app = await StartAuthenticationServerAsync ( options =>
6687 {
@@ -146,7 +167,11 @@ private async Task<WebApplication> StartAuthenticationServerAsync(Action<McpAuth
146167
147168 authenticationBuilder . AddScheme < McpAuthenticationOptions , McpAuthenticationHandler > ( McpAuthenticationDefaults . AuthenticationScheme , options =>
148169 {
149- options . ResourceMetadata = CreateMetadata ( ) ;
170+ options . ResourceMetadata = new ( )
171+ {
172+ AuthorizationServers = [ new Uri ( "https://localhost:7029" ) ] ,
173+ ScopesSupported = [ "mcp:tools" ] ,
174+ } ;
150175 configureOptions ? . Invoke ( options ) ;
151176 } ) ;
152177
@@ -167,12 +192,6 @@ private async Task<WebApplication> StartAuthenticationServerAsync(Action<McpAuth
167192 return app ;
168193 }
169194
170- private static ProtectedResourceMetadata CreateMetadata ( ) => new ( )
171- {
172- AuthorizationServers = [ new Uri ( "https://localhost:7029" ) ] ,
173- ScopesSupported = [ "mcp:tools" ] ,
174- } ;
175-
176195 private sealed class NoopBearerAuthenticationHandler (
177196 IOptionsMonitor < AuthenticationSchemeOptions > options ,
178197 ILoggerFactory logger ,
0 commit comments