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,247 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user