using System.Diagnostics; using System.Net; namespace ErsatzTV.Infrastructure.Tests.Streaming; /// /// Shared fakes for the HTTP-backed streaming services. Extracted from /// when /// needed the same harness. (ersatztv#511) /// internal sealed class StubHttpClientFactory(HttpMessageHandler handler) : IHttpClientFactory { public HttpClient CreateClient(string name) => new(handler, disposeHandler: false); } internal sealed class StatusCodeHttpMessageHandler(HttpStatusCode statusCode, string finalUri = null) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // HttpClient rewrites RequestMessage.RequestUri to the final hop when it follows a // redirect; finalUri lets a test stand in for "the media server answered this". if (finalUri is not null) { request.RequestUri = new Uri(finalUri); } return Task.FromResult(new HttpResponseMessage(statusCode) { RequestMessage = request }); } } internal sealed class FixedResponseHttpMessageHandler(HttpResponseMessage response) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); response.RequestMessage = request; return Task.FromResult(response); } } /// A readable stream that records how many bytes were actually pulled from it. internal sealed class TrackingStream(long length) : Stream { public int BytesRead { get; private set; } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => length; public override long Position { get => BytesRead; set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) { if (BytesRead >= length) { return 0; } int toRead = (int)Math.Min(count, length - BytesRead); Array.Clear(buffer, offset, toRead); BytesRead += toRead; return toRead; } public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } /// /// A readable stream that returns headers-worth of data instantly and then drips forever — /// the case that leaves outside /// . /// internal sealed class SlowDripStream : Stream { public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => 0; set => throw new NotSupportedException(); } public override async ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default) { await Task.Delay(Timeout.Infinite, cancellationToken); throw new UnreachableException(); } public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer.AsMemory(offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } internal sealed class ThrowingHttpMessageHandler(Exception exception) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromException(exception); } /// /// A handler that never completes until the request is cancelled — stands in for a host that /// accepts the connection and then hangs. /// internal sealed class HangingHttpMessageHandler : HttpMessageHandler { protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { await Task.Delay(Timeout.Infinite, cancellationToken); throw new UnreachableException(); } } /// /// A readable stream that yields bytes but only ever a little at a /// time, so a size cap has to be enforced during the copy rather than from Content-Length. /// internal sealed class ChunkedZeroStream(long length, int chunkSize = 4096) : Stream { private long _position; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => length; public override long Position { get => _position; set => throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) { if (_position >= length) { return 0; } int toRead = (int)Math.Min(Math.Min(count, chunkSize), length - _position); Array.Clear(buffer, offset, toRead); _position += toRead; return toRead; } public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); }