1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ using System . Net ;
5+ using System . Text . Json ;
6+ using System . Text . RegularExpressions ;
7+ using Microsoft365 . DeveloperProxy . Abstractions ;
8+
9+ namespace Microsoft365 . DeveloperProxy . Plugins . MockResponses ;
10+
11+ public class GraphMockResponsePlugin : MockResponsePlugin
12+ {
13+ public override string Name => nameof ( GraphMockResponsePlugin ) ;
14+
15+ protected override async Task OnRequest ( object ? sender , ProxyRequestArgs e )
16+ {
17+ if ( ! ProxyUtils . IsGraphBatchUrl ( e . Session . HttpClient . Request . RequestUri ) )
18+ {
19+ // not a batch request, use the basic mock functionality
20+ await base . OnRequest ( sender , e ) ;
21+ return ;
22+ }
23+
24+ var batch = JsonSerializer . Deserialize < GraphBatchRequestPayload > ( e . Session . HttpClient . Request . BodyString ) ;
25+ if ( batch == null )
26+ {
27+ await base . OnRequest ( sender , e ) ;
28+ return ;
29+ }
30+
31+ var responses = new List < GraphBatchResponsePayloadResponse > ( ) ;
32+ foreach ( var request in batch . Requests )
33+ {
34+ GraphBatchResponsePayloadResponse ? response = null ;
35+ var requestId = Guid . NewGuid ( ) . ToString ( ) ;
36+ var requestDate = DateTime . Now . ToString ( ) ;
37+ var headers = ProxyUtils
38+ . BuildGraphResponseHeaders ( e . Session . HttpClient . Request , requestId , requestDate )
39+ . ToDictionary ( h => h . Name , h => h . Value ) ;
40+
41+ var mockResponse = GetMatchingMockResponse ( request , e . Session . HttpClient . Request . RequestUri ) ;
42+ if ( mockResponse == null )
43+ {
44+ response = new GraphBatchResponsePayloadResponse
45+ {
46+ Id = request . Id ,
47+ Status = ( int ) HttpStatusCode . BadGateway ,
48+ Headers = headers ,
49+ Body = new GraphBatchResponsePayloadResponseBody
50+ {
51+ Error = new GraphBatchResponsePayloadResponseBodyError
52+ {
53+ Code = "BadGateway" ,
54+ Message = "No mock response found for this request"
55+ }
56+ }
57+ } ;
58+
59+ _logger ? . LogRequest ( new [ ] { $ "502 { request . Url } " } , MessageType . Mocked , new LoggingContext ( e . Session ) ) ;
60+ }
61+ else
62+ {
63+ dynamic ? body = null ;
64+ var statusCode = HttpStatusCode . OK ;
65+ if ( mockResponse . ResponseCode is not null )
66+ {
67+ statusCode = ( HttpStatusCode ) mockResponse . ResponseCode ;
68+ }
69+
70+ if ( mockResponse . ResponseHeaders is not null )
71+ {
72+ foreach ( var key in mockResponse . ResponseHeaders . Keys )
73+ {
74+ headers [ key ] = mockResponse . ResponseHeaders [ key ] ;
75+ }
76+ }
77+ // default the content type to application/json unless set in the mock response
78+ if ( ! headers . Any ( h => h . Key . Equals ( "content-type" , StringComparison . OrdinalIgnoreCase ) ) )
79+ {
80+ headers . Add ( "content-type" , "application/json" ) ;
81+ }
82+
83+ if ( mockResponse . ResponseBody is not null )
84+ {
85+ var bodyString = JsonSerializer . Serialize ( mockResponse . ResponseBody ) as string ;
86+ // we get a JSON string so need to start with the opening quote
87+ if ( bodyString ? . StartsWith ( "\" @" ) ?? false )
88+ {
89+ // we've got a mock body starting with @-token which means we're sending
90+ // a response from a file on disk
91+ // if we can read the file, we can immediately send the response and
92+ // skip the rest of the logic in this method
93+ // remove the surrounding quotes and the @-token
94+ var filePath = Path . Combine ( Path . GetDirectoryName ( _configuration . MocksFile ) ?? "" , ProxyUtils . ReplacePathTokens ( bodyString . Trim ( '"' ) . Substring ( 1 ) ) ) ;
95+ if ( ! File . Exists ( filePath ) )
96+ {
97+ _logger ? . LogError ( $ "File { filePath } not found. Serving file path in the mock response") ;
98+ body = bodyString ;
99+ }
100+ else
101+ {
102+ var bodyBytes = File . ReadAllBytes ( filePath ) ;
103+ body = Convert . ToBase64String ( bodyBytes ) ;
104+ }
105+ }
106+ else
107+ {
108+ body = mockResponse . ResponseBody ;
109+ }
110+ }
111+ response = new GraphBatchResponsePayloadResponse
112+ {
113+ Id = request . Id ,
114+ Status = ( int ) statusCode ,
115+ Headers = headers ,
116+ Body = body
117+ } ;
118+
119+ _logger ? . LogRequest ( new [ ] { $ "{ mockResponse . ResponseCode ?? 200 } { mockResponse . Url } " } , MessageType . Mocked , new LoggingContext ( e . Session ) ) ;
120+ }
121+
122+ responses . Add ( response ) ;
123+ }
124+
125+ var batchRequestId = Guid . NewGuid ( ) . ToString ( ) ;
126+ var batchRequestDate = DateTime . Now . ToString ( ) ;
127+ var batchHeaders = ProxyUtils . BuildGraphResponseHeaders ( e . Session . HttpClient . Request , batchRequestId , batchRequestDate ) ;
128+ var batchResponse = new GraphBatchResponsePayload
129+ {
130+ Responses = responses . ToArray ( )
131+ } ;
132+ e . Session . GenericResponse ( JsonSerializer . Serialize ( batchResponse ) , HttpStatusCode . OK , batchHeaders ) ;
133+ }
134+
135+ protected MockResponse ? GetMatchingMockResponse ( GraphBatchRequestPayloadRequest request , Uri batchRequestUri )
136+ {
137+ if ( _configuration . NoMocks ||
138+ _configuration . Responses is null ||
139+ ! _configuration . Responses . Any ( ) )
140+ {
141+ return null ;
142+ }
143+
144+ var mockResponse = _configuration . Responses . FirstOrDefault ( mockResponse =>
145+ {
146+ if ( mockResponse . Method != request . Method ) return false ;
147+ // URLs in batch are relative to Graph version number so we need
148+ // to make them absolute using the batch request URL
149+ var absoluteRequestFromBatchUrl = ProxyUtils
150+ . GetAbsoluteRequestUrlFromBatch ( batchRequestUri , request . Url )
151+ . ToString ( ) ;
152+ if ( mockResponse . Url == absoluteRequestFromBatchUrl )
153+ {
154+ return true ;
155+ }
156+
157+ // check if the URL contains a wildcard
158+ // if it doesn't, it's not a match for the current request for sure
159+ if ( ! mockResponse . Url . Contains ( '*' ) )
160+ {
161+ return false ;
162+ }
163+
164+ //turn mock URL with wildcard into a regex and match against the request URL
165+ var mockResponseUrlRegex = Regex . Escape ( mockResponse . Url ) . Replace ( "\\ *" , ".*" ) ;
166+ return Regex . IsMatch ( absoluteRequestFromBatchUrl , $ "^{ mockResponseUrlRegex } $") ;
167+ } ) ;
168+ return mockResponse ;
169+ }
170+ }
0 commit comments