using System.Net;
using System.Net.Http.Headers;
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Clients;
namespace DeepDrftTests;
///
/// Pins the Phase 21 Direction B bounded-Range request shape on .
/// The network-memory bound rests on each forward fetch being a finite bytes=start-end slice (so the
/// browser buffers only one segment), and on the caller learning the file total from the 206
/// Content-Range header (the EOF boundary the segment cursor advances toward). Both are request/response
/// plumbing the harness can observe directly; the actual browser memory behaviour is Daniel's manual re-run.
///
[TestFixture]
public class TrackMediaBoundedRangeTests
{
// Captures the outgoing Range header and returns a 206 with a Content-Range so TotalLength resolves.
private sealed class RangeCapturingHandler : HttpMessageHandler
{
private readonly long _total;
public RangeHeaderValue? CapturedRange { get; private set; }
public RangeCapturingHandler(long total) => _total = total;
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
CapturedRange = request.Headers.Range;
var from = request.Headers.Range?.Ranges.First().From ?? 0;
var to = request.Headers.Range?.Ranges.First().To ?? (_total - 1);
var body = new byte[to - from + 1];
var response = new HttpResponseMessage(HttpStatusCode.PartialContent)
{
Content = new ByteArrayContent(body),
};
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(from, to, _total);
return Task.FromResult(response);
}
}
private sealed class SingleClientFactory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
public SingleClientFactory(HttpMessageHandler handler) => _handler = handler;
public HttpClient CreateClient(string name) =>
new(_handler, disposeHandler: false) { BaseAddress = new Uri("https://content.test/") };
}
[Test]
public async Task GetTrackMedia_WithByteEnd_SendsBoundedRange()
{
var handler = new RangeCapturingHandler(total: 100_000_000);
var client = new TrackMediaClient(new SingleClientFactory(handler));
await client.GetTrackMedia("track-1", byteOffset: 0, byteEnd: 4 * 1024 * 1024 - 1, format: AudioFormat.Lossless);
var range = handler.CapturedRange!.Ranges.First();
Assert.Multiple(() =>
{
Assert.That(range.From, Is.EqualTo(0), "bounded request starts at the cursor");
Assert.That(range.To, Is.EqualTo(4 * 1024 * 1024 - 1),
"bounded request must carry an inclusive end so the server returns a finite slice (one segment)");
});
}
[Test]
public async Task GetTrackMedia_WithoutByteEnd_SendsOpenEndedRange()
{
var handler = new RangeCapturingHandler(total: 100_000_000);
var client = new TrackMediaClient(new SingleClientFactory(handler));
await client.GetTrackMedia("track-1", byteOffset: 1024, byteEnd: null, format: AudioFormat.Lossless);
var range = handler.CapturedRange!.Ranges.First();
Assert.Multiple(() =>
{
Assert.That(range.From, Is.EqualTo(1024), "open-ended request starts at the cursor");
Assert.That(range.To, Is.Null, "no byteEnd → open-ended bytes=start- (pre-Direction-B shape, kept working)");
});
}
[Test]
public async Task GetTrackMedia_206Response_SurfacesTotalLengthFromContentRange()
{
var handler = new RangeCapturingHandler(total: 970_000_000);
var client = new TrackMediaClient(new SingleClientFactory(handler));
var result = await client.GetTrackMedia("track-1", byteOffset: 0, byteEnd: 4 * 1024 * 1024 - 1);
Assert.That(result.Success, Is.True);
Assert.That(result.Value!.TotalLength, Is.EqualTo(970_000_000),
"the file total (the segment cursor's EOF boundary) must come from the 206 Content-Range");
}
}