mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
309 lines
13 KiB
C#
309 lines
13 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using RadicadeServer.Admin;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Rooms;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// Importing a subroom is three files, not one. A subroom loads a <c>.room</c> save, that save
|
|
/// references its data/audio blobs by name (<c>GET /data/<name>.htr</c>), and its Unity scene
|
|
/// is fetched through <c>/rooms/unity_assets/<name></c> by the name the bundle is stored as.
|
|
///
|
|
/// The admin upload route used to rename whatever it was handed to a fresh GUID and only accept
|
|
/// <c>.room</c>/<c>.htr</c>, so an import kept the room and lost the rest: every <c>.htr</c> and the
|
|
/// bundle answered 404 under the name the save pointed at. These pin the three names travelling
|
|
/// intact, the bundle binding, and the refusal of anything that is not one of the three.
|
|
/// </summary>
|
|
[Collection(DbFactoryCollection.Name)]
|
|
public class AdminRoomImportTests
|
|
{
|
|
private const string Secret = "room-import-test-key";
|
|
|
|
private static RecDBContext NewDb() => new(
|
|
TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options);
|
|
|
|
/// A CDN directory of its own, since every one of these routes writes into the storage root.
|
|
private sealed class Storage : IDisposable
|
|
{
|
|
private readonly string _previous = DBConfig.CDNStorage;
|
|
|
|
public Storage()
|
|
{
|
|
Root = Directory.CreateTempSubdirectory("radicade-room-import-").FullName;
|
|
DBConfig.CDNStorage = Root;
|
|
}
|
|
|
|
public string Root { get; }
|
|
|
|
public string Write(string name, string contents = "blob")
|
|
{
|
|
Directory.CreateDirectory(Root);
|
|
File.WriteAllText(Path.Join(Root, name), contents);
|
|
return name;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DBConfig.CDNStorage = _previous;
|
|
try { Directory.Delete(Root, recursive: true); } catch (IOException) { }
|
|
}
|
|
}
|
|
|
|
private sealed class AdminKey : IDisposable
|
|
{
|
|
private readonly string _previous = DBConfig.AdminApiKey;
|
|
|
|
public AdminKey() => DBConfig.AdminApiKey = Secret;
|
|
|
|
public void Dispose() => DBConfig.AdminApiKey = _previous;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ names
|
|
|
|
[Theory]
|
|
[InlineData("Showdown_0_1.assetbundle")]
|
|
[InlineData("8aas7b5ivtaggn9swhe3wkgdj.assetbundle")]
|
|
[InlineData(" Showdown_0_1.assetbundle ")]
|
|
public void AnUploadedBundleNameIsAccepted(string requested)
|
|
{
|
|
using var storage = new Storage();
|
|
storage.Write("Showdown_0_1.assetbundle");
|
|
storage.Write("8aas7b5ivtaggn9swhe3wkgdj.assetbundle");
|
|
|
|
Assert.True(AdminUtils.TryResolveBundleName(requested, out var name, out var error));
|
|
Assert.Null(error);
|
|
Assert.Equal(requested.Trim(), name);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("Showdown_0_1.room")]
|
|
[InlineData("Showdown_0_1")]
|
|
[InlineData("Showdown_0_1.assetbundle.bak")]
|
|
[InlineData("../Showdown_0_1.assetbundle")]
|
|
[InlineData("sub/Showdown_0_1.assetbundle")]
|
|
[InlineData("Home bundle.assetbundle")]
|
|
[InlineData("missing.assetbundle")]
|
|
public void AnythingThatIsNotAnUploadedBundleIsRefused(string? requested)
|
|
{
|
|
using var storage = new Storage();
|
|
storage.Write("Showdown_0_1.assetbundle");
|
|
|
|
Assert.False(AdminUtils.TryResolveBundleName(requested, out var name, out var error));
|
|
Assert.Null(name);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void BundleNamesAndRoomSaveNamesAreNotInterchangeable()
|
|
{
|
|
using var storage = new Storage();
|
|
storage.Write("Showdown.room");
|
|
storage.Write("Showdown_0_1.assetbundle");
|
|
|
|
// A .room is a save blob, never a subroom's Unity bundle.
|
|
Assert.False(AdminUtils.TryResolveBundleName("Showdown.room", out _, out _));
|
|
Assert.True(AdminUtils.TryResolveUploadName("Showdown.room", out var saveName, out _));
|
|
Assert.Equal("Showdown.room", saveName);
|
|
}
|
|
|
|
// ----------------------------------------------------------------- binding
|
|
|
|
[Fact]
|
|
public async Task BindingPointsTheSubroomAtTheBundleAndClearingTakesItBackOff()
|
|
{
|
|
using var storage = new Storage();
|
|
using var key = new AdminKey();
|
|
var bundle = storage.Write("Showdown_0_1.assetbundle");
|
|
storage.Write("Showdown.room");
|
|
|
|
await using var db = NewDb();
|
|
db.SubRooms.Add(new SubRoom { RoomId = 58, SubRoomId = 61, Name = "Home", DataBlob = "Showdown.room" });
|
|
await db.SaveChangesAsync();
|
|
var api = new AdminRoomAPI(db);
|
|
|
|
Assert.IsType<UnauthorizedObjectResult>(await api.SetBundle("wrong", 58, "Home", bundle));
|
|
Assert.IsType<NotFoundObjectResult>(await api.SetBundle(Secret, 59, "Home", bundle));
|
|
Assert.IsType<NotFoundObjectResult>(await api.SetBundle(Secret, 58, "Basement", bundle));
|
|
|
|
// A blob that is not a bundle, and a bundle that was never uploaded, are both refused
|
|
// rather than bound to a name the client would then fail to fetch.
|
|
Assert.IsType<BadRequestObjectResult>(await api.SetBundle(Secret, 58, "Home", "Showdown.room"));
|
|
Assert.IsType<BadRequestObjectResult>(await api.SetBundle(Secret, 58, "Home", "absent.assetbundle"));
|
|
|
|
Assert.IsType<OkObjectResult>(await api.SetBundle(Secret, 58, "Home", bundle));
|
|
db.ChangeTracker.Clear();
|
|
var stored = await db.SubRooms.SingleAsync();
|
|
Assert.Equal(bundle, stored.UnityAssetId);
|
|
Assert.Equal("Showdown.room", stored.DataBlob);
|
|
|
|
Assert.IsType<OkObjectResult>(await api.SetBundle(Secret, 58, "Home", ""));
|
|
db.ChangeTracker.Clear();
|
|
Assert.Equal("", (await db.SubRooms.SingleAsync()).UnityAssetId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreatingASubroomTakesTheBundleThatWasImportedWithIt()
|
|
{
|
|
using var storage = new Storage();
|
|
using var key = new AdminKey();
|
|
storage.Write("Showdown_0_1.assetbundle");
|
|
|
|
await using var db = NewDb();
|
|
db.Rooms.Add(new RoomData { RoomId = 58, Name = "Showdown", CreatorAccountId = 1, MaxPlayers = 6 });
|
|
await db.SaveChangesAsync();
|
|
var api = new AdminRoomAPI(db);
|
|
|
|
var created = await api.CreateSubRoom(Secret, 58, JsonNode.Parse("""
|
|
{
|
|
"Name": "Home",
|
|
"Accessibility": 1,
|
|
"MaxPlayers": 6,
|
|
"UnitySceneId": "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
|
"DataBlob": "Showdown.room",
|
|
"UnityAssetId": "Showdown_0_1.assetbundle"
|
|
}
|
|
""")!);
|
|
Assert.IsType<OkObjectResult>(created);
|
|
|
|
db.ChangeTracker.Clear();
|
|
var subRoom = await db.SubRooms.SingleAsync();
|
|
Assert.Equal("Showdown.room", subRoom.DataBlob);
|
|
Assert.Equal("Showdown_0_1.assetbundle", subRoom.UnityAssetId);
|
|
|
|
// A bundle that is not on disk fails the create instead of leaving a subroom pointing at
|
|
// nothing.
|
|
var failed = await api.CreateSubRoom(Secret, 58, JsonNode.Parse("""
|
|
{ "Name": "Basement", "DataBlob": "basement.room", "UnityAssetId": "absent.assetbundle" }
|
|
""")!);
|
|
Assert.IsType<BadRequestObjectResult>(failed);
|
|
db.ChangeTracker.Clear();
|
|
Assert.Single(await db.SubRooms.ToListAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreatingARoomTakesTheBundleForItsEntrySubroom()
|
|
{
|
|
using var storage = new Storage();
|
|
using var key = new AdminKey();
|
|
storage.Write("Showdown_0_1.assetbundle");
|
|
|
|
await using var db = NewDb();
|
|
var api = new AdminRoomAPI(db);
|
|
|
|
var created = await api.CreateRoom(Secret, 2, JsonNode.Parse("""
|
|
{
|
|
"Name": "Showdown",
|
|
"Accessibility": 1,
|
|
"MaxPlayers": 6,
|
|
"SubRooms": [
|
|
{
|
|
"Name": "Home",
|
|
"DataBlob": "Showdown.room",
|
|
"UnityAssetId": "Showdown_0_1.assetbundle"
|
|
}
|
|
]
|
|
}
|
|
""")!);
|
|
Assert.IsType<OkObjectResult>(created);
|
|
|
|
var subRoom = await db.SubRooms.SingleAsync();
|
|
Assert.Equal("Showdown.room", subRoom.DataBlob);
|
|
Assert.Equal("Showdown_0_1.assetbundle", subRoom.UnityAssetId);
|
|
}
|
|
|
|
// ----------------------------------------------------------------- uploads
|
|
|
|
/// The route itself, over real Kestrel and MVC model binding: whatever the admin sends is what
|
|
/// the CDN ends up holding, one file at a time, under the name it was sent with.
|
|
[Fact]
|
|
public async Task EachRoomFileLandsInTheCdnUnderTheNameItWasUploadedAs()
|
|
{
|
|
using var storage = new Storage();
|
|
using var key = new AdminKey();
|
|
|
|
var builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0");
|
|
builder.Logging.ClearProviders();
|
|
builder.Services.AddControllers().AddApplicationPart(typeof(AdminPanelAPI).Assembly);
|
|
builder.Services.AddCors(options => options.AddPolicy("AdminPanel", policy => policy.AllowAnyOrigin()));
|
|
builder.Services.Configure<FormOptions>(options => options.MultipartBodyLengthLimit = 256L * 1024 * 1024);
|
|
builder.Services.AddDbContext<RecDBContext>(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
|
|
await using var app = builder.Build();
|
|
app.UseCors();
|
|
app.MapControllers();
|
|
|
|
await app.StartAsync();
|
|
using var client = new HttpClient { BaseAddress = new Uri(app.Urls.Single()) };
|
|
|
|
async Task<(HttpStatusCode Status, JsonElement Body)> PostAsync(string fileName, byte[] bytes, string? name = null)
|
|
{
|
|
using var form = new MultipartFormDataContent();
|
|
form.Add(new ByteArrayContent(bytes), "file", fileName);
|
|
var query = $"/admin/panel/upload/room?key={Secret}" + (name == null ? "" : $"&name={Uri.EscapeDataString(name)}");
|
|
using var response = await client.PostAsync(query, form);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
var parsed = body.StartsWith('{') ? JsonDocument.Parse(body).RootElement : default;
|
|
return (response.StatusCode, parsed);
|
|
}
|
|
|
|
var (roomStatus, roomBody) = await PostAsync("Showdown.room", new byte[] { 1, 2, 3 });
|
|
Assert.Equal(HttpStatusCode.OK, roomStatus);
|
|
Assert.Equal("room", roomBody.GetProperty("kind").GetString());
|
|
Assert.Equal("Showdown.room", roomBody.GetProperty("name").GetString());
|
|
Assert.Equal(new byte[] { 1, 2, 3 }, await File.ReadAllBytesAsync(Path.Join(storage.Root, "Showdown.room")));
|
|
|
|
var htr = "315cb6bf6af14c0b83bfe2fdc8ef0c79.htr";
|
|
var (htrStatus, htrBody) = await PostAsync(htr, new byte[] { 4, 5 });
|
|
Assert.Equal(HttpStatusCode.OK, htrStatus);
|
|
Assert.Equal("htr", htrBody.GetProperty("kind").GetString());
|
|
Assert.True(File.Exists(Path.Join(storage.Root, htr)));
|
|
|
|
var (bundleStatus, bundleBody) = await PostAsync("Showdown_0_1.assetbundle", new byte[] { 6 });
|
|
Assert.Equal(HttpStatusCode.OK, bundleStatus);
|
|
Assert.Equal("assetbundle", bundleBody.GetProperty("kind").GetString());
|
|
Assert.True(File.Exists(Path.Join(storage.Root, "Showdown_0_1.assetbundle")));
|
|
|
|
// An explicit ?name= still wins, which is how the Data Import page stores a renamed file.
|
|
var (renamedStatus, _) = await PostAsync("whatever.bin", new byte[] { 7 }, name: "renamed.room");
|
|
Assert.Equal(HttpStatusCode.OK, renamedStatus);
|
|
Assert.True(File.Exists(Path.Join(storage.Root, "renamed.room")));
|
|
|
|
var (refusedStatus, _) = await PostAsync("archive.zip", new byte[] { 8 });
|
|
Assert.Equal(HttpStatusCode.BadRequest, refusedStatus);
|
|
Assert.False(File.Exists(Path.Join(storage.Root, "archive.zip")));
|
|
|
|
// A name the CDN could never be asked for is refused rather than quietly renamed.
|
|
var (oddNameStatus, _) = await PostAsync("spot the odd name.room", new byte[] { 9 });
|
|
Assert.Equal(HttpStatusCode.BadRequest, oddNameStatus);
|
|
|
|
Assert.Equal(
|
|
new[] { "renamed.room", "Showdown.room" }.OrderBy(x => x),
|
|
Directory.GetFiles(storage.Root, "*.room").Select(Path.GetFileName).OrderBy(x => x));
|
|
|
|
// The uploaded bundle is now something the panel can list and bind.
|
|
using var uploads = await client.GetAsync($"/admin/panel/uploads?key={Secret}");
|
|
var uploadsBody = await uploads.Content.ReadAsStringAsync();
|
|
Assert.Contains("Showdown_0_1.assetbundle", uploadsBody);
|
|
Assert.Contains("assetbundles", uploadsBody);
|
|
|
|
await app.StopAsync();
|
|
}
|
|
}
|