mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.CDN;
|
||||
using RadicadeServer.DB;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The client's Unity asset bundle download is resumable: its string table carries
|
||||
/// "bytes={0}-", "bytes={0}-{1}", "Accept-Ranges" and "Content-Range", so it asks the CDN for
|
||||
/// byte ranges rather than a whole file. Every route that serves those bytes must therefore
|
||||
/// process ranges — a range answer that returns the entire body makes the client re-transfer
|
||||
/// the whole file once per range, which is how a 73 MB Studio bundle came to spend 228 s in
|
||||
/// AssetBundle_Download, outlive the room's Photon connection, and fail the load with
|
||||
/// code 6706-3. Dropping <c>enableRangeProcessing</c> is invisible at runtime (the download
|
||||
/// still completes, just ~20x slower and past the network timeout), so it is pinned here.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class CdnRangeRequestTests : IDisposable
|
||||
{
|
||||
private const string BlobName = "recdump-0cfd85d85c7b905dfbb83a6f3d3c887b.room";
|
||||
|
||||
private readonly string root = Directory.CreateTempSubdirectory("radicade-cdn-range-test-").FullName;
|
||||
private readonly string? previousStorage;
|
||||
|
||||
public CdnRangeRequestTests()
|
||||
{
|
||||
previousStorage = DBConfig.CDNStorage;
|
||||
DBConfig.CDNStorage = root;
|
||||
File.WriteAllText(Path.Join(root, BlobName), "blob");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DBConfig.CDNStorage = previousStorage!;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(root, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string ImmutableCache = "public, max-age=31536000, immutable";
|
||||
private static readonly string ReplaceableCache = "public, max-age=86400";
|
||||
|
||||
private const string BundleName = "0f0f0f0f-0f0f-0f0f-0f0f-0f0f0f0f0f0f.assetbundle";
|
||||
|
||||
private static T WithHttpContext<T>(T controller) where T : Controller
|
||||
{
|
||||
controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() };
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static PhysicalFileResult RangeServed(IActionResult result)
|
||||
{
|
||||
var file = Assert.IsType<PhysicalFileResult>(result);
|
||||
Assert.True(file.EnableRangeProcessing, $"{file.FileName} is served without range processing");
|
||||
Assert.Equal("application/octet-stream", file.ContentType);
|
||||
return file;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheDataRouteThatCarriesAssetBundlesProcessesRanges()
|
||||
{
|
||||
// /data/{blob} is where the bundle bytes land: /rooms/unity_assets/... hands the
|
||||
// client a Filename and nothing else, so this is the URL the downloader ranges over.
|
||||
var file = RangeServed(new DataCDN().GetDataBlob(BlobName));
|
||||
|
||||
Assert.Equal(Path.Join(root, BlobName), file.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheRoomAndUnityAssetRoutesProcessRanges()
|
||||
{
|
||||
RangeServed(new RoomsCDN().GetDataBlob(BlobName));
|
||||
RangeServed(new RoomsCDN().GetUnityAssetRaw(BlobName, 0, 1));
|
||||
RangeServed(new RoomsCDN().GetUnityAssetRaw(BlobName, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheInventionRouteProcessesRanges()
|
||||
{
|
||||
RangeServed(new InventionCDN().GetDataBlob(BlobName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheBlobRoutesTellClientsTheyMayCacheWhatTheyDownloaded()
|
||||
{
|
||||
// A Studio room's bundle is the payload that decides whether the room loads at all.
|
||||
// With no cache directive the client's HTTP stack is free to fetch all of it again
|
||||
// on the next join, at ~100 ms RTT, and the room load is exactly as slow as it was
|
||||
// the first time.
|
||||
File.WriteAllText(Path.Join(root, BundleName), "bundle");
|
||||
|
||||
var data = WithHttpContext(new DataCDN());
|
||||
data.GetDataBlob(BundleName);
|
||||
Assert.Equal(ImmutableCache, data.Response.Headers["Cache-Control"].ToString());
|
||||
|
||||
var rooms = WithHttpContext(new RoomsCDN());
|
||||
rooms.GetUnityAssetRaw(BundleName, 0, 1);
|
||||
Assert.Equal(ImmutableCache, rooms.Response.Headers["Cache-Control"].ToString());
|
||||
|
||||
// Room saves are content-addressed too, but admin repairs replace one under its
|
||||
// existing name, so they must expire rather than be pinned forever.
|
||||
data = WithHttpContext(new DataCDN());
|
||||
data.GetDataBlob(BlobName);
|
||||
Assert.Equal(ReplaceableCache, data.Response.Headers["Cache-Control"].ToString());
|
||||
|
||||
rooms = WithHttpContext(new RoomsCDN());
|
||||
rooms.GetDataBlob(BlobName);
|
||||
Assert.Equal(ReplaceableCache, rooms.Response.Headers["Cache-Control"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnUnknownBlobStillAnswersNotFound()
|
||||
{
|
||||
// Range support must not turn a missing file into a body, the way an unhandled
|
||||
// unsatisfiable range can.
|
||||
Assert.IsType<NotFoundResult>(new DataCDN().GetDataBlob("not-on-disk.room"));
|
||||
Assert.IsType<NotFoundResult>(new RoomsCDN().GetDataBlob("not-on-disk.room"));
|
||||
Assert.IsType<NotFoundResult>(new RoomsCDN().GetUnityAssetRaw("not-on-disk.room", 0, 1));
|
||||
Assert.IsType<NotFoundResult>(new InventionCDN().GetDataBlob("not-on-disk.room"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user