mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
326 lines
14 KiB
C#
326 lines
14 KiB
C#
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);
|
|
}
|
|
}
|