Tests/VSAFakeHttp.ps1
|
# Shared test transport (F-67). # # Since v1.5.1 the module has a single HTTP stack built on System.Net.Http.HttpClient, so tests no # longer simulate failures by mocking Invoke-RestMethod. Instead they install a fake # HttpMessageHandler, which is the supported way to test HttpClient code: the real transport runs -- # its retry loop, status handling, Retry-After parsing, body reading and error typing are all # genuinely exercised -- while the network is replaced by a scripted queue of responses. # # The injection seam is the module's own client cache ($script:VSAHttpClients), so no test-only hook # exists in production code. Tests must clear that cache afterwards (see Reset-VSAFakeTransport # usage) so subsequent code rebuilds a real client. # # NOTE: this file is deliberately NOT named *.Tests.ps1 -- it is dot-sourced by test files, not run # as a test file itself. if (-not ('FakeHttpMessageHandler' -as [type])) { $FakeHttpSource = @' using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; public class FakeHttpExchange { public int Status; public string Body; public int RetryAfterSeconds; public bool Fault; } // Returns scripted responses in FIFO order and records what was actually sent. public class FakeHttpMessageHandler : HttpMessageHandler { public Queue<FakeHttpExchange> Script = new Queue<FakeHttpExchange>(); public List<string> RequestUris = new List<string>(); public List<string> RequestBodies = new List<string>(); public List<string> ContentTypes = new List<string>(); public List<string> AuthHeaders = new List<string>(); public List<string> Methods = new List<string>(); public int CallCount = 0; public void EnqueueResponse(int status, string body, int retryAfterSeconds) { Script.Enqueue(new FakeHttpExchange { Status = status, Body = body, RetryAfterSeconds = retryAfterSeconds, Fault = false }); } // Simulates no HTTP response at all (socket reset / unreachable / blocked endpoint). public void EnqueueFault() { Script.Enqueue(new FakeHttpExchange { Fault = true }); } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { CallCount++; Methods.Add(request.Method.Method); RequestUris.Add(request.RequestUri.ToString()); AuthHeaders.Add(request.Headers.Contains("Authorization") ? string.Join(",", request.Headers.GetValues("Authorization")) : null); if (request.Content != null) { RequestBodies.Add(request.Content.ReadAsStringAsync().Result); ContentTypes.Add(request.Content.Headers.Contains("Content-Type") ? string.Join(",", request.Content.Headers.GetValues("Content-Type")) : null); } else { RequestBodies.Add(null); ContentTypes.Add(null); } if (Script.Count == 0) throw new InvalidOperationException("FakeHttpMessageHandler: no scripted exchange left for " + request.RequestUri); FakeHttpExchange x = Script.Dequeue(); if (x.Fault) throw new HttpRequestException("simulated connection reset"); var resp = new HttpResponseMessage((HttpStatusCode)x.Status); resp.Content = new StringContent(x.Body ?? string.Empty, Encoding.UTF8, "application/json"); if (x.RetryAfterSeconds > 0) resp.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(x.RetryAfterSeconds)); return Task.FromResult(resp); } } '@ # The framework reference set differs by edition; 5.1 resolves System.Net.Http from the GAC. $FakeHttpRefs = if ($PSVersionTable.PSEdition -eq 'Core') { @('System.Net.Http', 'System.Net.Primitives', 'System.Runtime', 'System.Collections', 'System.Threading.Tasks', 'netstandard') } else { @('System.Net.Http') } Add-Type -TypeDefinition $FakeHttpSource -ReferencedAssemblies $FakeHttpRefs -ErrorAction Stop } |