1+ using FastMoq . Extensions ;
2+ using FastMoq . AzureFunctions . Http ;
3+ using Microsoft . Azure . Functions . Worker ;
4+ using Microsoft . Azure . Functions . Worker . Http ;
5+ using System . Text ;
6+ using System . Text . Json ;
7+
8+ namespace FastMoq . AzureFunctions . Extensions
9+ {
10+ /// <summary>
11+ /// Provides Azure Functions HTTP-trigger helpers for constructing request and response data in tests.
12+ /// </summary>
13+ public static class HttpTriggerTestExtensions
14+ {
15+ /// <summary>
16+ /// Creates a concrete <see cref="HttpRequestData" /> for the current <see cref="Mocker" /> instance.
17+ /// </summary>
18+ /// <param name="mocker">The current <see cref="Mocker" /> instance.</param>
19+ /// <param name="configureRequest">Optional request-builder configuration.</param>
20+ /// <returns>A concrete <see cref="HttpRequestData" /> suitable for Azure Functions trigger tests.</returns>
21+ public static HttpRequestData CreateHttpRequestData ( this Mocker mocker , Action < HttpRequestDataBuilder > ? configureRequest = null )
22+ {
23+ ArgumentNullException . ThrowIfNull ( mocker ) ;
24+
25+ return GetOrCreateConfiguredFunctionContext ( mocker ) . CreateHttpRequestData ( configureRequest ) ;
26+ }
27+
28+ /// <summary>
29+ /// Creates a concrete <see cref="HttpRequestData" /> for the supplied <see cref="FunctionContext" />.
30+ /// </summary>
31+ /// <param name="functionContext">The function context to associate with the request.</param>
32+ /// <param name="configureRequest">Optional request-builder configuration.</param>
33+ /// <returns>A concrete <see cref="HttpRequestData" /> suitable for Azure Functions trigger tests.</returns>
34+ public static HttpRequestData CreateHttpRequestData ( this FunctionContext functionContext , Action < HttpRequestDataBuilder > ? configureRequest = null )
35+ {
36+ ArgumentNullException . ThrowIfNull ( functionContext ) ;
37+
38+ var builder = new HttpRequestDataBuilder ( functionContext ) ;
39+ configureRequest ? . Invoke ( builder ) ;
40+ return builder . Build ( ) ;
41+ }
42+
43+ /// <summary>
44+ /// Creates a concrete <see cref="HttpResponseData" /> for the current <see cref="Mocker" /> instance.
45+ /// </summary>
46+ /// <param name="mocker">The current <see cref="Mocker" /> instance.</param>
47+ /// <param name="configureResponse">Optional response-builder configuration.</param>
48+ /// <returns>A concrete <see cref="HttpResponseData" /> suitable for Azure Functions trigger tests.</returns>
49+ public static HttpResponseData CreateHttpResponseData ( this Mocker mocker , Action < HttpResponseDataBuilder > ? configureResponse = null )
50+ {
51+ ArgumentNullException . ThrowIfNull ( mocker ) ;
52+
53+ return GetOrCreateConfiguredFunctionContext ( mocker ) . CreateHttpResponseData ( configureResponse ) ;
54+ }
55+
56+ /// <summary>
57+ /// Creates a concrete <see cref="HttpResponseData" /> for the supplied <see cref="FunctionContext" />.
58+ /// </summary>
59+ /// <param name="functionContext">The function context to associate with the response.</param>
60+ /// <param name="configureResponse">Optional response-builder configuration.</param>
61+ /// <returns>A concrete <see cref="HttpResponseData" /> suitable for Azure Functions trigger tests.</returns>
62+ public static HttpResponseData CreateHttpResponseData ( this FunctionContext functionContext , Action < HttpResponseDataBuilder > ? configureResponse = null )
63+ {
64+ ArgumentNullException . ThrowIfNull ( functionContext ) ;
65+
66+ var builder = new HttpResponseDataBuilder ( functionContext ) ;
67+ configureResponse ? . Invoke ( builder ) ;
68+ return builder . Build ( ) ;
69+ }
70+
71+ /// <summary>
72+ /// Reads the current request body as a string and rewinds the stream when possible.
73+ /// </summary>
74+ /// <param name="request">The current request.</param>
75+ /// <param name="encoding">The text encoding. Defaults to UTF-8.</param>
76+ /// <returns>The body text.</returns>
77+ public static Task < string > ReadBodyAsStringAsync ( this HttpRequestData request , Encoding ? encoding = null )
78+ {
79+ ArgumentNullException . ThrowIfNull ( request ) ;
80+
81+ return ReadStreamAsStringAsync ( request . Body , encoding ) ;
82+ }
83+
84+ /// <summary>
85+ /// Reads the current request body as JSON and rewinds the stream when possible.
86+ /// </summary>
87+ /// <typeparam name="TValue">The expected JSON type.</typeparam>
88+ /// <param name="request">The current request.</param>
89+ /// <param name="jsonSerializerOptions">Optional serializer options.</param>
90+ /// <param name="cancellationToken">The cancellation token.</param>
91+ /// <returns>The deserialized body value.</returns>
92+ public static Task < TValue ? > ReadBodyAsJsonAsync < TValue > ( this HttpRequestData request , JsonSerializerOptions ? jsonSerializerOptions = null , CancellationToken cancellationToken = default )
93+ {
94+ ArgumentNullException . ThrowIfNull ( request ) ;
95+
96+ return ReadStreamAsJsonAsync < TValue > ( request . Body , jsonSerializerOptions , cancellationToken ) ;
97+ }
98+
99+ /// <summary>
100+ /// Reads the current response body as a string and rewinds the stream when possible.
101+ /// </summary>
102+ /// <param name="response">The current response.</param>
103+ /// <param name="encoding">The text encoding. Defaults to UTF-8.</param>
104+ /// <returns>The body text.</returns>
105+ public static Task < string > ReadBodyAsStringAsync ( this HttpResponseData response , Encoding ? encoding = null )
106+ {
107+ ArgumentNullException . ThrowIfNull ( response ) ;
108+
109+ return ReadStreamAsStringAsync ( response . Body , encoding ) ;
110+ }
111+
112+ /// <summary>
113+ /// Reads the current response body as JSON and rewinds the stream when possible.
114+ /// </summary>
115+ /// <typeparam name="TValue">The expected JSON type.</typeparam>
116+ /// <param name="response">The current response.</param>
117+ /// <param name="jsonSerializerOptions">Optional serializer options.</param>
118+ /// <param name="cancellationToken">The cancellation token.</param>
119+ /// <returns>The deserialized body value.</returns>
120+ public static Task < TValue ? > ReadBodyAsJsonAsync < TValue > ( this HttpResponseData response , JsonSerializerOptions ? jsonSerializerOptions = null , CancellationToken cancellationToken = default )
121+ {
122+ ArgumentNullException . ThrowIfNull ( response ) ;
123+
124+ return ReadStreamAsJsonAsync < TValue > ( response . Body , jsonSerializerOptions , cancellationToken ) ;
125+ }
126+
127+ private static FunctionContext GetOrCreateConfiguredFunctionContext ( Mocker mocker )
128+ {
129+ var containsFunctionContext = mocker . Contains ( typeof ( FunctionContext ) ) ;
130+ if ( containsFunctionContext )
131+ {
132+ var existingFunctionContext = mocker . GetObject < FunctionContext > ( ) ;
133+ if ( existingFunctionContext ? . InstanceServices is not null )
134+ {
135+ return existingFunctionContext ;
136+ }
137+ }
138+
139+ if ( mocker . HasTypeRegistration ( typeof ( IServiceProvider ) ) )
140+ {
141+ mocker . AddFunctionContextInstanceServices ( mocker . GetRequiredObject < IServiceProvider > ( ) , replace : true ) ;
142+ return mocker . GetRequiredObject < FunctionContext > ( ) ;
143+ }
144+
145+ mocker . AddFunctionContextInstanceServices ( replace : containsFunctionContext ) ;
146+ return mocker . GetRequiredObject < FunctionContext > ( ) ;
147+ }
148+
149+ private static async Task < TValue ? > ReadStreamAsJsonAsync < TValue > ( Stream stream , JsonSerializerOptions ? jsonSerializerOptions , CancellationToken cancellationToken )
150+ {
151+ ResetStreamPosition ( stream ) ;
152+ var value = await JsonSerializer . DeserializeAsync < TValue > ( stream , jsonSerializerOptions , cancellationToken ) . ConfigureAwait ( false ) ;
153+ ResetStreamPosition ( stream ) ;
154+ return value ;
155+ }
156+
157+ private static async Task < string > ReadStreamAsStringAsync ( Stream stream , Encoding ? encoding )
158+ {
159+ ResetStreamPosition ( stream ) ;
160+
161+ using var reader = new StreamReader ( stream , encoding ?? Encoding . UTF8 , detectEncodingFromByteOrderMarks : true , leaveOpen : true ) ;
162+ var value = await reader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
163+
164+ ResetStreamPosition ( stream ) ;
165+ return value ;
166+ }
167+
168+ private static void ResetStreamPosition ( Stream stream )
169+ {
170+ if ( stream . CanSeek )
171+ {
172+ stream . Position = 0 ;
173+ }
174+ }
175+ }
176+ }
0 commit comments