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();
}