mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-27 07:59:03 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Relationships.V1;
|
||||
using RadicadeServer.API.Relationships.V2;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.CDN;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Images;
|
||||
using RadicadeServer.RecNet.Relationships;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Wire-contract tests for the images and relationships endpoints: the key
|
||||
/// spellings the 2023 client deserializes, the cheer-state isolation between
|
||||
/// images, and the pending-request guard on accepting friend requests.
|
||||
/// </summary>
|
||||
public class ImagesAndRelationshipsTests
|
||||
{
|
||||
private const long CallerId = 42;
|
||||
private const long OtherId = 77;
|
||||
|
||||
private static RecDBContext CreateDb()
|
||||
{
|
||||
var options = TestDbOptions.Create()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new RecDBContext(options);
|
||||
}
|
||||
|
||||
private static T CreateController<T>(RecDBContext db, long? userId = CallerId) where T : Controller
|
||||
{
|
||||
var identity = new ClaimsIdentity("TestAuth");
|
||||
if (userId.HasValue)
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString()));
|
||||
|
||||
var controller = (T)Activator.CreateInstance(typeof(T), db)!;
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
||||
};
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static ImageDB Photo(long id, long owner, SavedImageAccessibility access,
|
||||
SavedImageType type, long? roomId = null, string? tags = null) => new()
|
||||
{
|
||||
Id = id,
|
||||
Name = $"img{id}.png",
|
||||
OwnerId = owner,
|
||||
Accessibility = access,
|
||||
ImageType = type,
|
||||
RoomId = roomId,
|
||||
PlayerIds = tags ?? "[]",
|
||||
CreatedAt = new DateTime(2026, 1, 1).AddHours(id)
|
||||
};
|
||||
|
||||
// ---- images ------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task PlayerPhotoList_ServesDualKeyProjection()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Images.AddRange(
|
||||
Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera),
|
||||
Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera),
|
||||
Photo(3, CallerId, SavedImageAccessibility.Private, SavedImageType.ShareCamera));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RadicadeServer.API.ImagesAPI>(db);
|
||||
var result = await api.GetImagesFromPlayer(CallerId);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
|
||||
// Newest first: image 2 leads, image 1 follows.
|
||||
Assert.Equal(2L, (long)result[0].GetType().GetProperty("Id")!.GetValue(result[0])!);
|
||||
|
||||
var first = JsonSerializer.Serialize(result[1]);
|
||||
using var doc = JsonDocument.Parse(first);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Both key spellings must be present: the raw projection (Id/Type) that
|
||||
// captured 2022 responses used, and the ImagesPlayer projection
|
||||
// (SavedImageId/SavedImageType) the 2023 client's photo grids read.
|
||||
Assert.Equal(1, root.GetProperty("Id").GetInt64());
|
||||
Assert.Equal(1, root.GetProperty("SavedImageId").GetInt64());
|
||||
Assert.Equal(1, root.GetProperty("Type").GetInt32());
|
||||
Assert.Equal(1, root.GetProperty("SavedImageType").GetInt32());
|
||||
Assert.Equal("img1.png", root.GetProperty("ImageName").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheerImage_IsIsolatedPerImage()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Images.AddRange(
|
||||
Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera),
|
||||
Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RadicadeServer.API.ImagesAPI>(db);
|
||||
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
||||
api.ControllerContext.HttpContext.Request.Body =
|
||||
new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new ImageCheerRequest { SavedImageId = 1, Cheer = true }));
|
||||
|
||||
var result = await api.CheerImage(new ImageCheerRequest { SavedImageId = 1, Cheer = true });
|
||||
Assert.True(result.success);
|
||||
|
||||
// One row, tied to image 1 only - the old code matched on the player
|
||||
// alone, so this cheer would have clobbered cheers on every other image.
|
||||
var interactions = await db.ImageInteractions.ToListAsync();
|
||||
var interaction = Assert.Single(interactions);
|
||||
Assert.Equal(1, interaction.SavedImageId);
|
||||
Assert.True(interaction.IsCheered);
|
||||
|
||||
// Cheering image 2 now adds a second row rather than stealing the first.
|
||||
api.ControllerContext.HttpContext.Request.Body =
|
||||
new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new ImageCheerRequest { SavedImageId = 2, Cheer = true }));
|
||||
await api.CheerImage(new ImageCheerRequest { SavedImageId = 2, Cheer = true });
|
||||
|
||||
var all = await db.ImageInteractions.ToListAsync();
|
||||
Assert.Equal(2, all.Count);
|
||||
Assert.Contains(all, x => x.SavedImageId == 1 && x.IsCheered);
|
||||
Assert.Contains(all, x => x.SavedImageId == 2 && x.IsCheered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheerLookup_ReportsCallerStateInRequestOrder()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Images.AddRange(
|
||||
Photo(1, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera),
|
||||
Photo(2, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera));
|
||||
db.ImageInteractions.Add(new CheeredImage { SavedImageId = 2, PlayerId = CallerId, IsCheered = true });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RadicadeServer.API.ImagesAPI>(db);
|
||||
var result = await api.GetBulkImageCheers(new long[] { 1, 2 });
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
var first = Assert.IsType<long>(result[0].GetType().GetProperty("SavedImageId")!.GetValue(result[0]));
|
||||
Assert.Equal(1, first);
|
||||
Assert.False((bool)result[0].GetType().GetProperty("IsCheered")!.GetValue(result[0])!);
|
||||
Assert.True((bool)result[1].GetType().GetProperty("IsCheered")!.GetValue(result[1])!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlayerFeed_IncludesTaggedPhotos()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Images.AddRange(
|
||||
Photo(1, OtherId, SavedImageAccessibility.Public, SavedImageType.ShareCamera, tags: "[42]"),
|
||||
Photo(2, OtherId, SavedImageAccessibility.Public, SavedImageType.ShareCamera, tags: "[99]"),
|
||||
Photo(3, CallerId, SavedImageAccessibility.Public, SavedImageType.ShareCamera));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RadicadeServer.API.ImagesAPI>(db);
|
||||
var feed = await api.GetPlayerFeed(CallerId);
|
||||
|
||||
Assert.Equal(2, feed.Count); // own photo + tagged photo, not the other tag
|
||||
}
|
||||
|
||||
// ---- relationships -------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task RelationshipWireShape_UsesPlayerID()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Relationships.Add(new RealRelationship
|
||||
{
|
||||
AccountId = CallerId,
|
||||
PlayerId = OtherId,
|
||||
RelationshipType = RelationshipType.Friend,
|
||||
Favorited = ReciprocalStatus.Local
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RelationshipsV2>(db);
|
||||
var relationships = await api.GetRelationships();
|
||||
|
||||
var relationship = Assert.Single(relationships);
|
||||
var json = JsonSerializer.Serialize(relationship);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// The captured real RecNet shape: { Id, PlayerID, RelationshipType, ... }.
|
||||
// The old PlayerId spelling failed to bind on the client, blanking the
|
||||
// friends list.
|
||||
Assert.True(root.TryGetProperty("PlayerID", out var playerId));
|
||||
Assert.Equal(OtherId, playerId.GetInt64());
|
||||
Assert.True(root.TryGetProperty("Id", out _));
|
||||
Assert.Equal(3, root.GetProperty("RelationshipType").GetInt32());
|
||||
Assert.False(root.TryGetProperty("PlayerId", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendThenAcceptFriendRequest_BefriendsBothSides()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
|
||||
var sender = CreateController<RelationshipsV2>(db, CallerId);
|
||||
await sender.SendFriendRequest(OtherId);
|
||||
|
||||
var senderRel = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId);
|
||||
var receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId);
|
||||
Assert.Equal(RelationshipType.FriendRequestSent, senderRel.RelationshipType);
|
||||
Assert.Equal(RelationshipType.FriendRequestReceived, receiverRel.RelationshipType);
|
||||
|
||||
// A third party cannot accept a request addressed to someone else.
|
||||
var stranger = CreateController<RelationshipsV2>(db, 999);
|
||||
await stranger.AcceptFriendRequest(CallerId);
|
||||
receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId);
|
||||
Assert.Equal(RelationshipType.FriendRequestReceived, receiverRel.RelationshipType);
|
||||
|
||||
// The actual receiver accepts: both sides flip to Friend.
|
||||
var receiver = CreateController<RelationshipsV2>(db, OtherId);
|
||||
await receiver.AcceptFriendRequest(CallerId);
|
||||
|
||||
senderRel = await db.Relationships.SingleAsync(x => x.AccountId == CallerId && x.PlayerId == OtherId);
|
||||
receiverRel = await db.Relationships.SingleAsync(x => x.AccountId == OtherId && x.PlayerId == CallerId);
|
||||
Assert.Equal(RelationshipType.Friend, senderRel.RelationshipType);
|
||||
Assert.Equal(RelationshipType.Friend, receiverRel.RelationshipType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcceptWithoutPendingRequest_DoesNothing()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
var receiver = CreateController<RelationshipsV2>(db, OtherId);
|
||||
await receiver.AcceptFriendRequest(CallerId);
|
||||
|
||||
// No rows were created at all: accepting out of the blue is a no-op,
|
||||
// not a silent friendship.
|
||||
Assert.Empty(await db.Relationships.ToListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveFriend_ClearsBothSides()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Relationships.AddRange(
|
||||
new RealRelationship { AccountId = CallerId, PlayerId = OtherId, RelationshipType = RelationshipType.Friend },
|
||||
new RealRelationship { AccountId = OtherId, PlayerId = CallerId, RelationshipType = RelationshipType.Friend });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RelationshipsV2>(db);
|
||||
await api.RemoveFriend(OtherId);
|
||||
|
||||
Assert.All(await db.Relationships.ToListAsync(),
|
||||
x => Assert.Equal(RelationshipType.None, x.RelationshipType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FriendsList_OnlyServesAcceptedFriendships()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
db.Relationships.AddRange(
|
||||
new RealRelationship { AccountId = CallerId, PlayerId = OtherId, RelationshipType = RelationshipType.Friend },
|
||||
new RealRelationship { AccountId = CallerId, PlayerId = 5, RelationshipType = RelationshipType.FriendRequestSent });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = CreateController<RelationshipsV2>(db);
|
||||
var friends = await api.GetFriends();
|
||||
|
||||
var friend = Assert.Single(friends);
|
||||
Assert.Equal(OtherId, friend.PlayerID);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RelationshipWith_ServesRowOrBareNone()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
var api = CreateController<RelationshipsV1>(db);
|
||||
|
||||
// No history yet: a bare None projection, not an error.
|
||||
var result = await api.RelationshipWith(OtherId);
|
||||
var ok = Assert.IsType<OkObjectResult>(result);
|
||||
using var noneDoc = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
|
||||
Assert.Equal(0, noneDoc.RootElement.GetProperty("RelationshipType").GetInt32());
|
||||
Assert.Equal(OtherId, noneDoc.RootElement.GetProperty("PlayerID").GetInt64());
|
||||
|
||||
db.Relationships.Add(new RealRelationship
|
||||
{
|
||||
AccountId = CallerId,
|
||||
PlayerId = OtherId,
|
||||
RelationshipType = RelationshipType.Friend
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var friendResult = await api.RelationshipWith(OtherId);
|
||||
var ok2 = Assert.IsType<OkObjectResult>(friendResult);
|
||||
using var friendDoc = JsonDocument.Parse(JsonSerializer.Serialize(ok2.Value));
|
||||
Assert.Equal(OtherId, friendDoc.RootElement.GetProperty("PlayerID").GetInt64());
|
||||
Assert.Equal(3, friendDoc.RootElement.GetProperty("RelationshipType").GetInt32());
|
||||
}
|
||||
|
||||
// ---- photo tagging ---------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task PhotoTaggingSetting_RoundTrips()
|
||||
{
|
||||
using var db = CreateDb();
|
||||
var api = CreateController<RadicadeServer.API.Players.V1.PlayersV1>(db);
|
||||
|
||||
// Unset reads 0 as a bare integer.
|
||||
var initial = await api.GetPhotoTaggingSetting();
|
||||
var initialOk = Assert.IsType<OkObjectResult>(initial);
|
||||
Assert.Equal(0, initialOk.Value);
|
||||
|
||||
// Write 2 through a JSON body, then read it back.
|
||||
api.ControllerContext.HttpContext.Request.ContentType = "application/json";
|
||||
api.ControllerContext.HttpContext.Request.Body =
|
||||
new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(new { Setting = 2 }));
|
||||
var written = await api.SetPhotoTaggingSetting();
|
||||
Assert.Equal(2, Assert.IsType<OkObjectResult>(written).Value);
|
||||
|
||||
api.ControllerContext.HttpContext.Request.Body = new MemoryStream();
|
||||
var read = await api.GetPhotoTaggingSetting();
|
||||
Assert.Equal(2, Assert.IsType<OkObjectResult>(read).Value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user