Files
Radicade/ServerTests/ImageAssetResolutionTests.cs
T
2026-10-16 23:33:13 -07:00

130 lines
4.6 KiB
C#

using System;
using System.IO;
using RadicadeServer.Images;
using Xunit;
namespace RadicadeServer.Tests;
/// <summary>
/// The image route answers by name, and the name a client asks for is not always the name on
/// disk. Two real cases drove this: a custom-avatar design is stored as
/// <c>&lt;guid&gt;.bin</c> while its bytes are a PNG, and the default room image ships as
/// <c>DefaultRoomImage.png</c> while the server asks for the bare <c>DefaultRoomImage</c>.
/// Both resolved to nothing, and nothing is not "no picture" -- it is an empty 404 body, which
/// the 2023 client turns into a broken surface (and, for a room image, a fatal load failure).
/// </summary>
public class ImageAssetResolutionTests : IDisposable
{
private readonly string root = Directory.CreateTempSubdirectory("radicade-images-test-").FullName;
public void Dispose()
{
try
{
Directory.Delete(root, true);
}
catch (IOException)
{
}
}
private string Write(string name, string content = "x")
{
var path = Path.Join(root, name);
File.WriteAllText(path, content);
return path;
}
[Fact]
public void TheRequestedNameIsFoundFirst()
{
var expected = Write("photo.png");
Assert.Equal(expected, ImageAPI.ResolveAsset("photo.png", new[] { root }));
}
[Fact]
public void ADesignStoredAsBinIsFoundByTheNameTheClientAsksFor()
{
// Exactly what the customizer wrote before designs moved to the image CDN: the bytes
// are a PNG, the name on disk ends in .bin, and the client asks for that name.
var expected = Write("a07188bb-9f99-442d-a2ff-2aebad2ca7fe.bin");
Assert.Equal(expected, ImageAPI.ResolveAsset("a07188bb-9f99-442d-a2ff-2aebad2ca7fe.bin", new[] { root }));
}
[Fact]
public void AMissingExtensionIsTriedOnTheSameStem()
{
// The default-image case: the server asks for "DefaultRoomImage", the deployment ships
// "DefaultRoomImage.png", and the extension search used to be skipped for this name.
var expected = Write("DefaultRoomImage.png");
Assert.Equal(expected, ImageAPI.ResolveAsset("DefaultRoomImage", new[] { root }));
}
[Fact]
public void AnExtensionThatDoesNotDescribeTheContentStillFindsTheFile()
{
var expected = Write("design.png");
Assert.Equal(expected, ImageAPI.ResolveAsset("design.bin", new[] { root }));
Assert.Equal(expected, ImageAPI.ResolveAsset("design.jpeg", new[] { root }));
}
[Fact]
public void RootsAreSearchedInOrder()
{
var first = Directory.CreateDirectory(Path.Join(root, "images")).FullName;
var second = Directory.CreateDirectory(Path.Join(root, "custom")).FullName;
var expected = Path.Join(first, "shirt.png");
File.WriteAllText(expected, "x");
File.WriteAllText(Path.Join(second, "shirt.png"), "x");
Assert.Equal(expected, ImageAPI.ResolveAsset("shirt.png", new[] { first, second }));
}
[Fact]
public void ANamedRootIsPreferredOverALaterRootsVariant()
{
var images = Directory.CreateDirectory(Path.Join(root, "images")).FullName;
var custom = Directory.CreateDirectory(Path.Join(root, "custom")).FullName;
var expected = Path.Join(custom, "shirt.bin");
File.WriteAllText(expected, "x");
File.WriteAllText(Path.Join(images, "shirt.png"), "x");
Assert.Equal(expected, ImageAPI.ResolveAsset("shirt.bin", new[] { images, custom }));
}
[Theory]
[InlineData("")]
[InlineData(".")]
[InlineData("..")]
[InlineData("../outside.png")]
[InlineData("..\\outside.png")]
[InlineData("sub/../outside.png")]
public void NamesThatDoNotNameAFileInsideARootResolveToNothing(string name)
{
Assert.Null(ImageAPI.ResolveAsset(name, new[] { root }));
}
[Fact]
public void AnUnknownNameResolvesToNothing()
{
Assert.Null(ImageAPI.ResolveAsset("definitely-missing.png", new[] { root }));
Assert.Null(ImageAPI.ResolveAsset("definitely-missing.bin", new[] { root }));
}
[Fact]
public void TheServedRootsStillIncludeTheDesignRoot()
{
// A design written before designs moved to the image CDN only stays reachable while
// CDN/Custom is still searched, so dropping it here would silently blank old shirts.
var roots = ImageAPI.AssetRoots();
Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Images")));
Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Custom")));
Assert.Contains(roots, r => r.EndsWith(Path.Join("CDN", "Storage")));
}
}