4
4
exchangeAuthorization ,
5
5
refreshAuthorization ,
6
6
registerClient ,
7
+ discoverOAuthProtectedResourceMetadata ,
8
+ extractResourceMetadataUrl ,
7
9
} from "./auth.js" ;
8
10
9
11
// Mock fetch globally
@@ -15,6 +17,168 @@ describe("OAuth Authorization", () => {
15
17
mockFetch . mockReset ( ) ;
16
18
} ) ;
17
19
20
+ describe ( "extractResourceMetadataUrl" , ( ) => {
21
+ it ( "returns resource metadata url when present" , async ( ) => {
22
+ const resourceUrl = "https://resource.example.com/.well-known/oauth-protected-resource"
23
+ const mockResponse = {
24
+ headers : {
25
+ get : jest . fn ( ( name ) => name === "WWW-Authenticate" ? `Bearer realm="mcp", resource_metadata="${ resourceUrl } "` : null ) ,
26
+ }
27
+ } as unknown as Response
28
+
29
+ expect ( extractResourceMetadataUrl ( mockResponse ) ) . toEqual ( new URL ( resourceUrl ) ) ;
30
+ } ) ;
31
+
32
+ it ( "returns undefined if not bearer" , async ( ) => {
33
+ const resourceUrl = "https://resource.example.com/.well-known/oauth-protected-resource"
34
+ const mockResponse = {
35
+ headers : {
36
+ get : jest . fn ( ( name ) => name === "WWW-Authenticate" ? `Basic realm="mcp", resource_metadata="${ resourceUrl } "` : null ) ,
37
+ }
38
+ } as unknown as Response
39
+
40
+ expect ( extractResourceMetadataUrl ( mockResponse ) ) . toBeUndefined ( ) ;
41
+ } ) ;
42
+
43
+ it ( "returns undefined if resource_metadata not present" , async ( ) => {
44
+ const mockResponse = {
45
+ headers : {
46
+ get : jest . fn ( ( name ) => name === "WWW-Authenticate" ? `Basic realm="mcp"` : null ) ,
47
+ }
48
+ } as unknown as Response
49
+
50
+ expect ( extractResourceMetadataUrl ( mockResponse ) ) . toBeUndefined ( ) ;
51
+ } ) ;
52
+
53
+ it ( "returns undefined on invalid url" , async ( ) => {
54
+ const resourceUrl = "invalid-url"
55
+ const mockResponse = {
56
+ headers : {
57
+ get : jest . fn ( ( name ) => name === "WWW-Authenticate" ? `Basic realm="mcp", resource_metadata="${ resourceUrl } "` : null ) ,
58
+ }
59
+ } as unknown as Response
60
+
61
+ expect ( extractResourceMetadataUrl ( mockResponse ) ) . toBeUndefined ( ) ;
62
+ } ) ;
63
+ } ) ;
64
+
65
+ describe ( "discoverOAuthProtectedResourceMetadata" , ( ) => {
66
+ const validMetadata = {
67
+ resource : "https://resource.example.com" ,
68
+ authorization_servers : [ "https://auth.example.com" ] ,
69
+ } ;
70
+
71
+ it ( "returns metadata when discovery succeeds" , async ( ) => {
72
+ mockFetch . mockResolvedValueOnce ( {
73
+ ok : true ,
74
+ status : 200 ,
75
+ json : async ( ) => validMetadata ,
76
+ } ) ;
77
+
78
+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) ;
79
+ expect ( metadata ) . toEqual ( validMetadata ) ;
80
+ const calls = mockFetch . mock . calls ;
81
+ expect ( calls . length ) . toBe ( 1 ) ;
82
+ const [ url , options ] = calls [ 0 ] ;
83
+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
84
+ expect ( options . headers ) . toEqual ( {
85
+ "MCP-Protocol-Version" : "2024-11-05"
86
+ } ) ;
87
+ } ) ;
88
+
89
+ it ( "returns metadata when first fetch fails but second without MCP header succeeds" , async ( ) => {
90
+ // Set up a counter to control behavior
91
+ let callCount = 0 ;
92
+
93
+ // Mock implementation that changes behavior based on call count
94
+ mockFetch . mockImplementation ( ( _url , _options ) => {
95
+ callCount ++ ;
96
+
97
+ if ( callCount === 1 ) {
98
+ // First call with MCP header - fail with TypeError (simulating CORS error)
99
+ // We need to use TypeError specifically because that's what the implementation checks for
100
+ return Promise . reject ( new TypeError ( "Network error" ) ) ;
101
+ } else {
102
+ // Second call without header - succeed
103
+ return Promise . resolve ( {
104
+ ok : true ,
105
+ status : 200 ,
106
+ json : async ( ) => validMetadata
107
+ } ) ;
108
+ }
109
+ } ) ;
110
+
111
+ // Should succeed with the second call
112
+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) ;
113
+ expect ( metadata ) . toEqual ( validMetadata ) ;
114
+
115
+ // Verify both calls were made
116
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
117
+
118
+ // Verify first call had MCP header
119
+ expect ( mockFetch . mock . calls [ 0 ] [ 1 ] ?. headers ) . toHaveProperty ( "MCP-Protocol-Version" ) ;
120
+ } ) ;
121
+
122
+ it ( "throws an error when all fetch attempts fail" , async ( ) => {
123
+ // Set up a counter to control behavior
124
+ let callCount = 0 ;
125
+
126
+ // Mock implementation that changes behavior based on call count
127
+ mockFetch . mockImplementation ( ( _url , _options ) => {
128
+ callCount ++ ;
129
+
130
+ if ( callCount === 1 ) {
131
+ // First call - fail with TypeError
132
+ return Promise . reject ( new TypeError ( "First failure" ) ) ;
133
+ } else {
134
+ // Second call - fail with different error
135
+ return Promise . reject ( new Error ( "Second failure" ) ) ;
136
+ }
137
+ } ) ;
138
+
139
+ // Should fail with the second error
140
+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
141
+ . rejects . toThrow ( "Second failure" ) ;
142
+
143
+ // Verify both calls were made
144
+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
145
+ } ) ;
146
+
147
+ it ( "throws on 404 errors" , async ( ) => {
148
+ mockFetch . mockResolvedValueOnce ( {
149
+ ok : false ,
150
+ status : 404 ,
151
+ } ) ;
152
+
153
+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
154
+ . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
155
+ } ) ;
156
+
157
+ it ( "throws on non-404 errors" , async ( ) => {
158
+ mockFetch . mockResolvedValueOnce ( {
159
+ ok : false ,
160
+ status : 500 ,
161
+ } ) ;
162
+
163
+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
164
+ . rejects . toThrow ( "HTTP 500" ) ;
165
+ } ) ;
166
+
167
+ it ( "validates metadata schema" , async ( ) => {
168
+ mockFetch . mockResolvedValueOnce ( {
169
+ ok : true ,
170
+ status : 200 ,
171
+ json : async ( ) => ( {
172
+ // Missing required fields
173
+ scopes_supported : [ "email" , "mcp" ] ,
174
+ } ) ,
175
+ } ) ;
176
+
177
+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
178
+ . rejects . toThrow ( ) ;
179
+ } ) ;
180
+ } ) ;
181
+
18
182
describe ( "discoverOAuthMetadata" , ( ) => {
19
183
const validMetadata = {
20
184
issuer : "https://auth.example.com" ,
0 commit comments