mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
248 lines
8.9 KiB
C#
248 lines
8.9 KiB
C#
using System.Reflection;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.Admin;
|
|
using RadicadeServer.Clubs;
|
|
using RadicadeServer.DB;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// Setting a club's image from the admin panel: <c>POST admin/clubs/upload/image</c> and
|
|
/// <c>GET admin/clubs/set/image</c>.
|
|
///
|
|
/// Clubs are JSON in the "Clubs" server config rather than a table, so the panel's generic table
|
|
/// routes cannot reach one and these two endpoints are the whole of the write path. The name they
|
|
/// store is the one <see cref="ClubStore.BuildClub"/> hands the client as MainImageName, so the
|
|
/// image the panel picks is the image the game shows.
|
|
/// </summary>
|
|
[Collection(DbFactoryCollection.Name)]
|
|
public class AdminClubImageTests
|
|
{
|
|
private const string Secret = "test-club-image-secret";
|
|
private const long OwnerId = 77;
|
|
|
|
private static RecDBContext NewDb() => new(
|
|
TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options);
|
|
|
|
private static async Task<long> CreateClubAsync(RecDBContext db)
|
|
{
|
|
var club = await ClubStore.CreateAsync(db, OwnerId, "Image Test Club", "a club");
|
|
return club.clubId;
|
|
}
|
|
|
|
private static IFormFile FormFile(string name, byte[] bytes) =>
|
|
new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", name);
|
|
|
|
// A real PNG header, so the file has the bytes its extension claims.
|
|
private static readonly byte[] PngBytes = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
|
|
|
/// <summary>Runs an action with the admin secret and CDN image folder pointed at a temp dir.</summary>
|
|
private static async Task<T> WithAdminAsync<T>(string imagesDirectory, Func<Task<T>> action)
|
|
{
|
|
var originalKey = DBConfig.AdminApiKey;
|
|
var originalImages = DBConfig.CDNImages;
|
|
DBConfig.AdminApiKey = Secret;
|
|
DBConfig.CDNImages = imagesDirectory;
|
|
try
|
|
{
|
|
return await action();
|
|
}
|
|
finally
|
|
{
|
|
DBConfig.AdminApiKey = originalKey;
|
|
DBConfig.CDNImages = originalImages;
|
|
}
|
|
}
|
|
|
|
private static string TempImages() =>
|
|
Path.Combine(Path.GetTempPath(), "radicade-club-image-" + Guid.NewGuid().ToString("N"));
|
|
|
|
[Fact]
|
|
public void ThePanelCallsTheseRoutes()
|
|
{
|
|
var setImage = typeof(AdminClubAPI).GetMethod(nameof(AdminClubAPI.SetImage))!;
|
|
Assert.Contains("set/image", setImage
|
|
.GetCustomAttributes(typeof(HttpMethodAttribute), false)
|
|
.Cast<HttpMethodAttribute>()
|
|
.Select(attribute => attribute.Template));
|
|
|
|
var upload = typeof(AdminClubAPI).GetMethod(nameof(AdminClubAPI.UploadImage))!;
|
|
Assert.Contains("upload/image", upload
|
|
.GetCustomAttributes(typeof(HttpMethodAttribute), false)
|
|
.Cast<HttpMethodAttribute>()
|
|
.Select(attribute => attribute.Template));
|
|
|
|
// The controller's own prefix is what the panel's fetch URLs are built from.
|
|
var prefix = typeof(AdminClubAPI).GetCustomAttributes(typeof(RouteAttribute), false)
|
|
.Cast<RouteAttribute>()
|
|
.Single();
|
|
Assert.Equal("admin/clubs", prefix.Template);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetImageNamesTheImageTheGameIsServed()
|
|
{
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
var result = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "custom-club.png"));
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal("custom-club.png", club!.imageName);
|
|
|
|
// What the client is actually told is the name that matters.
|
|
var served = (Dictionary<string, object?>)ClubStore.BuildClub(club, OwnerId);
|
|
Assert.Equal("custom-club.png", served["MainImageName"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnEmptyNamePutsTheClubBackOnTheDefaultImage()
|
|
{
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "something.png"));
|
|
// Clearing is the same call with an empty name, which is what the panel sends.
|
|
var result = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, ""));
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal(ClubStore.DefaultImageName, club!.imageName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetImageKeepsOnlyTheFileName()
|
|
{
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
// An image name is a key into the CDN's image folder, so a path must not survive.
|
|
await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId, "../../elsewhere/evil.png"));
|
|
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal("evil.png", club!.imageName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetImageRefusesAnInvalidKeyAndAnUnknownClub()
|
|
{
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
var wrongKey = await api.SetImage("not-the-secret", clubId, "x.png");
|
|
Assert.IsType<UnauthorizedObjectResult>(wrongKey);
|
|
|
|
var missing = await WithAdminAsync(TempImages(), () => api.SetImage(Secret, clubId + 9999, "x.png"));
|
|
Assert.IsType<NotFoundObjectResult>(missing);
|
|
|
|
// Neither attempt touched the club.
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal(ClubStore.DefaultImageName, club!.imageName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadStoresTheFileAndNamesItOnTheClub()
|
|
{
|
|
var directory = TempImages();
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
try
|
|
{
|
|
var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("logo.png", PngBytes)));
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.EndsWith(".png", club!.imageName);
|
|
Assert.True(File.Exists(Path.Join(directory, club.imageName)));
|
|
|
|
// The response names the URL the panel shows, so it has to point at the stored file.
|
|
Assert.Contains($"/images/img/{club.imageName}", ok.Value!.ToString());
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadRefusesAFileThatIsNotAnImageAndLeavesNothingBehind()
|
|
{
|
|
var directory = TempImages();
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
try
|
|
{
|
|
var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("save.room", PngBytes)));
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
Assert.False(Directory.Exists(directory));
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal(ClubStore.DefaultImageName, club!.imageName);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadRefusesAnEmptyFile()
|
|
{
|
|
var directory = TempImages();
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
try
|
|
{
|
|
var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId, FormFile("logo.png", [])));
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
var club = await ClubStore.GetAsync(db, clubId);
|
|
Assert.Equal(ClubStore.DefaultImageName, club!.imageName);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadOnAnUnknownClubWritesNoFile()
|
|
{
|
|
var directory = TempImages();
|
|
var db = NewDb();
|
|
var clubId = await CreateClubAsync(db);
|
|
var api = new AdminClubAPI(db);
|
|
|
|
try
|
|
{
|
|
// The club is looked up before the upload is stored, so a mistyped club id cannot
|
|
// leave an orphaned file in the CDN.
|
|
var result = await WithAdminAsync(directory, () => api.UploadImage(Secret, clubId + 9999, FormFile("logo.png", PngBytes)));
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
Assert.False(Directory.Exists(directory));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(directory)) Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
}
|