mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
344 lines
16 KiB
C#
344 lines
16 KiB
C#
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Routing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Matchmaking;
|
|
using RadicadeServer.Clubs;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Accounts;
|
|
using RadicadeServer.RecNet.Enums;
|
|
using RadicadeServer.RecNet.Enums.Rooms;
|
|
using RadicadeServer.RecNet.Responses;
|
|
using RadicadeServer.RecNet.Rooms;
|
|
using RadicadeServer.Utils;
|
|
using System.Text.Json;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
[Collection(DbFactoryCollection.Name)]
|
|
public class ClubMatchmakingRouteTests
|
|
{
|
|
private const long CallerId = 101;
|
|
private const long FriendId = 202;
|
|
private const long StrangerId = 303;
|
|
private const long RoomId = 404;
|
|
|
|
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
|
|
{
|
|
private readonly DbContextOptions<RecDBContext> options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
public RecDBContext CreateDbContext() => new(options);
|
|
}
|
|
|
|
private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext()
|
|
{
|
|
var factory = new TestDbContextFactory();
|
|
RoomUtils.DbFactory = factory;
|
|
MatchmakingUtils.DbFactory = factory;
|
|
return (factory, factory.CreateDbContext());
|
|
}
|
|
|
|
private static void SetUser(Controller controller, long accountId)
|
|
{
|
|
var identity = new ClaimsIdentity("TestAuth");
|
|
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString()));
|
|
controller.ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
|
};
|
|
}
|
|
|
|
private static void SetFormBody(Controller controller, string body)
|
|
{
|
|
var request = controller.HttpContext.Request;
|
|
request.ContentType = "application/x-www-form-urlencoded";
|
|
request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body));
|
|
request.ContentLength = request.Body.Length;
|
|
}
|
|
|
|
[Fact]
|
|
public void SupportsLegacyAndNewWatchClubJoinAliases()
|
|
{
|
|
var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GoToClub))!;
|
|
var routes = method.GetCustomAttributes(typeof(HttpMethodAttribute), false)
|
|
.Cast<HttpMethodAttribute>()
|
|
.Select(x => x.Template ?? string.Empty)
|
|
.ToList();
|
|
Assert.Contains("goto/club/{clubId}", routes);
|
|
Assert.Contains("matchmake/club/{clubId}", routes);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegistersFriendFollowingRoute()
|
|
{
|
|
var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.MatchmakePlayer))!;
|
|
var routes = method.GetCustomAttributes(typeof(HttpPostAttribute), false)
|
|
.Cast<HttpPostAttribute>()
|
|
.Select(attribute => attribute.Template);
|
|
|
|
Assert.Contains("matchmake/player/{targetId:long}", routes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FollowRequiresFriendshipAndEntersTheFriendsCurrentInstance()
|
|
{
|
|
var (factory, db) = CreateContext();
|
|
await using var _db = db;
|
|
|
|
db.Rooms.Add(new RoomData
|
|
{
|
|
RoomId = RoomId,
|
|
Name = "Friend Room",
|
|
CreatorAccountId = FriendId,
|
|
Accessibility = Accessibility.Public,
|
|
CreatedAt = DateTime.UtcNow,
|
|
});
|
|
var instance = new RoomInstance
|
|
{
|
|
roomId = RoomId,
|
|
subRoomId = 1,
|
|
roomInstanceType = RoomInstanceType.Public,
|
|
maxCapacity = 10,
|
|
photonRoomId = "friend-photon-room",
|
|
};
|
|
db.RoomInstances.Add(instance);
|
|
db.Presences.AddRange(
|
|
new RealPresence { playerId = CallerId },
|
|
new RealPresence { playerId = FriendId, isOnline = true, roomInstance = instance.roomInstanceId },
|
|
new RealPresence { playerId = StrangerId, isOnline = true, roomInstance = instance.roomInstanceId });
|
|
db.Relationships.Add(new RadicadeServer.RecNet.Relationships.RealRelationship
|
|
{
|
|
AccountId = FriendId,
|
|
PlayerId = CallerId,
|
|
RelationshipType = RadicadeServer.RecNet.Relationships.RelationshipType.Friend,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = new MatchmakingAPI(db);
|
|
SetUser(api, CallerId);
|
|
var followed = await api.MatchmakePlayer(FriendId, null);
|
|
|
|
Assert.NotNull(followed);
|
|
Assert.Equal(JoinStatus.Success, followed!.errorCode);
|
|
Assert.Equal(instance.roomInstanceId, followed.roomInstance?.roomInstanceId);
|
|
Assert.Equal("friend-photon-room", followed.roomInstance?.photonRoomId);
|
|
|
|
await using var verify = factory.CreateDbContext();
|
|
Assert.Equal(instance.roomInstanceId, (await verify.Presences.SingleAsync(x => x.playerId == CallerId)).roomInstance);
|
|
|
|
var nonFriendApi = new MatchmakingAPI(verify);
|
|
SetUser(nonFriendApi, CallerId);
|
|
var refused = await nonFriendApi.MatchmakePlayer(StrangerId, null);
|
|
Assert.NotNull(refused);
|
|
Assert.Equal(JoinStatus.NoSuchRoom, refused!.errorCode);
|
|
Assert.Null(refused.roomInstance);
|
|
|
|
var selfFollow = await nonFriendApi.MatchmakePlayer(CallerId, null);
|
|
Assert.NotNull(selfFollow);
|
|
Assert.Equal(JoinStatus.NoSuchRoom, selfFollow!.errorCode);
|
|
Assert.Null(selfFollow.roomInstance);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FollowRefusesRoomBans()
|
|
{
|
|
var (factory, db) = CreateContext();
|
|
await using var _db = db;
|
|
|
|
db.Rooms.Add(new RoomData
|
|
{
|
|
RoomId = RoomId,
|
|
Name = "Friend Room",
|
|
CreatorAccountId = FriendId,
|
|
Accessibility = Accessibility.Public,
|
|
CreatedAt = DateTime.UtcNow,
|
|
});
|
|
var instance = new RoomInstance { roomId = RoomId, maxCapacity = 10 };
|
|
db.RoomInstances.Add(instance);
|
|
db.Presences.AddRange(
|
|
new RealPresence { playerId = CallerId },
|
|
new RealPresence { playerId = FriendId, isOnline = true, roomInstance = instance.roomInstanceId });
|
|
db.Relationships.Add(new RadicadeServer.RecNet.Relationships.RealRelationship
|
|
{
|
|
AccountId = CallerId,
|
|
PlayerId = FriendId,
|
|
RelationshipType = RadicadeServer.RecNet.Relationships.RelationshipType.Friend,
|
|
});
|
|
db.RoomBans.Add(new RoomBan { roomId = RoomId, bannedPlayerId = CallerId, actorId = FriendId });
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = new MatchmakingAPI(db);
|
|
SetUser(api, CallerId);
|
|
var refused = await api.MatchmakePlayer(FriendId, null);
|
|
|
|
Assert.NotNull(refused);
|
|
Assert.Equal(JoinStatus.BannedFromRoom, refused!.errorCode);
|
|
Assert.Null(refused.roomInstance);
|
|
await using var verify = factory.CreateDbContext();
|
|
Assert.Null((await verify.Presences.SingleAsync(x => x.playerId == CallerId)).roomInstance);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClubOwnerIsCreatorMemberAndLegacyClubRoutesAreRegistered()
|
|
{
|
|
var options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
await using var db = new RecDBContext(options);
|
|
var club = await ClubStore.CreateAsync(db, CallerId, "My club", "Description");
|
|
|
|
var creator = Assert.Single(ClubStore.GetMembers(club), member => member.AccountId == CallerId);
|
|
Assert.Equal(ClubMembershipTypes.Creator, creator.MembershipType);
|
|
Assert.Equal(club.createdAt, creator.CreatedAt);
|
|
|
|
var leave = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.LeaveClub))!;
|
|
var leaveRoutes = leave.GetCustomAttributes<HttpPostAttribute>()
|
|
.Select(attribute => attribute.Template)
|
|
.ToArray();
|
|
Assert.Contains("{clubId:long}/members/leave", leaveRoutes);
|
|
Assert.DoesNotContain("~/clubs/club/{clubId:long}/members/leave", leaveRoutes);
|
|
var minLevel = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.SetMinimumLevel))!;
|
|
var minLevelRoutes = minLevel.GetCustomAttributes<HttpPutAttribute>()
|
|
.Select(attribute => attribute.Template)
|
|
.ToArray();
|
|
Assert.Contains("{clubId:long}/minlevel", minLevelRoutes);
|
|
// The class route "clubs/club" already answers clubs/club/{clubId}/minlevel. The absolute
|
|
// copy of that same path registered the template twice, so every call to it threw
|
|
// AmbiguousMatchException (500) instead of reaching the handler.
|
|
Assert.DoesNotContain("~/clubs/club/{clubId:long}/minlevel", minLevelRoutes);
|
|
|
|
await ClubStore.SetMemberTypeAsync(db, club.clubId, FriendId,
|
|
ClubMembershipTypes.Member, CallerId);
|
|
var leaveApi = new ClubsAPI(db);
|
|
SetUser(leaveApi, FriendId);
|
|
var left = Assert.IsType<OkObjectResult>(await leaveApi.LeaveClub(club.clubId));
|
|
Assert.NotNull(left.Value);
|
|
Assert.DoesNotContain(ClubStore.GetMembers(await ClubStore.GetAsync(db, club.clubId)),
|
|
member => member.AccountId == FriendId);
|
|
|
|
var creatorLeave = new ClubsAPI(db);
|
|
SetUser(creatorLeave, CallerId);
|
|
Assert.IsType<ObjectResult>(await creatorLeave.LeaveClub(club.clubId));
|
|
Assert.Contains(ClubStore.GetMembers(await ClubStore.GetAsync(db, club.clubId)),
|
|
member => member.AccountId == CallerId && member.MembershipType == ClubMembershipTypes.Creator);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClubMainImageCanBeReadAndUpdatedByCoowners()
|
|
{
|
|
var options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
await using var db = new RecDBContext(options);
|
|
var club = await ClubStore.CreateAsync(db, CallerId, "Image club", "Description");
|
|
|
|
var api = new ClubsAPI(db);
|
|
SetUser(api, CallerId);
|
|
var read = Assert.IsType<OkObjectResult>(await api.GetClubMainImage(club.clubId));
|
|
using (var initial = JsonDocument.Parse(JsonSerializer.Serialize(read.Value)))
|
|
{
|
|
Assert.Equal("DefaultImgPurple", initial.RootElement.GetProperty("value")
|
|
.GetProperty("Club").GetProperty("MainImageName").GetString());
|
|
}
|
|
|
|
SetFormBody(api, "imageName=2026-07-12%2Fclub.jpg");
|
|
var updated = Assert.IsType<OkObjectResult>(await api.SetClubMainImage(club.clubId));
|
|
using (var response = JsonDocument.Parse(JsonSerializer.Serialize(updated.Value)))
|
|
{
|
|
Assert.Equal("2026-07-12/club.jpg", response.RootElement.GetProperty("value")
|
|
.GetProperty("Club").GetProperty("MainImageName").GetString());
|
|
}
|
|
|
|
var reread = Assert.IsType<OkObjectResult>(await api.GetClubMainImage(club.clubId));
|
|
using (var response = JsonDocument.Parse(JsonSerializer.Serialize(reread.Value)))
|
|
{
|
|
Assert.Equal("2026-07-12/club.jpg", response.RootElement.GetProperty("value")
|
|
.GetProperty("Club").GetProperty("MainImageName").GetString());
|
|
}
|
|
|
|
var missingImageApi = new ClubsAPI(db);
|
|
SetUser(missingImageApi, CallerId);
|
|
SetFormBody(missingImageApi, "");
|
|
Assert.IsType<BadRequestObjectResult>(await missingImageApi.SetClubMainImage(club.clubId));
|
|
|
|
var memberApi = new ClubsAPI(db);
|
|
SetUser(memberApi, FriendId);
|
|
SetFormBody(memberApi, "imageName=not-allowed.jpg");
|
|
var forbidden = Assert.IsType<ObjectResult>(await memberApi.SetClubMainImage(club.clubId));
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
|
|
var getRouteTemplates = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubMainImage))!
|
|
.GetCustomAttributes<HttpGetAttribute>().Select(attribute => attribute.Template).ToArray();
|
|
Assert.Contains("{clubId:long}/mainimage", getRouteTemplates);
|
|
Assert.Contains("~/club/{clubId:long}/mainimage", getRouteTemplates);
|
|
var putRouteTemplates = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.SetClubMainImage))!
|
|
.GetCustomAttributes<HttpPutAttribute>().Select(attribute => attribute.Template).ToArray();
|
|
Assert.Contains("{clubId:long}/mainimage", putRouteTemplates);
|
|
Assert.Contains("~/club/{clubId:long}/mainimage", putRouteTemplates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClubHomeAndSubscriptionShapesMatchTheReferenceContracts()
|
|
{
|
|
var options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
await using var db = new RecDBContext(options);
|
|
var club = await ClubStore.CreateAsync(db, CallerId, "Home club", "Description");
|
|
await ClubStore.SetClubhouseAsync(db, club.clubId, RoomId);
|
|
await ClubStore.SetHomeClubAsync(db, CallerId, club.clubId);
|
|
|
|
var home = await ClubStore.GetHomeClubAsync(db, CallerId);
|
|
Assert.Equal(club.clubId, home?.clubId);
|
|
using var clubJson = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildClub(home, CallerId)));
|
|
Assert.Equal(JsonValueKind.Number, clubJson.RootElement.GetProperty("ClubId").ValueKind);
|
|
Assert.False(clubJson.RootElement.TryGetProperty("isOwner", out _));
|
|
Assert.False(clubJson.RootElement.TryGetProperty("isMember", out _));
|
|
Assert.False(clubJson.RootElement.TryGetProperty("createdAt", out _));
|
|
var expectedClubKeys = new[]
|
|
{
|
|
"ClubId", "Name", "Description", "Category", "Visibility", "Joinability", "AllowJuniors",
|
|
"MainImageName", "ClubType", "ClubhouseRoomId", "CreatorAccountId", "IsRRO", "MinLevel",
|
|
"State", "MemberCount",
|
|
};
|
|
Assert.Equal(expectedClubKeys.OrderBy(key => key, StringComparer.Ordinal),
|
|
clubJson.RootElement.EnumerateObject().Select(property => property.Name).OrderBy(key => key, StringComparer.Ordinal));
|
|
|
|
club.members.Add(new ClubMemberRecord
|
|
{
|
|
accountId = StrangerId,
|
|
membershipType = ClubMembershipTypes.PendingRequested,
|
|
inviterAccountId = CallerId,
|
|
});
|
|
var clubMembers = ClubStore.GetMembers(club);
|
|
Assert.Contains(clubMembers, member => member.AccountId == StrangerId &&
|
|
member.MembershipType == ClubMembershipTypes.PendingRequested);
|
|
var requestedMember = Assert.Single(clubMembers, member => member.AccountId == StrangerId);
|
|
using var memberJson = JsonDocument.Parse(JsonSerializer.Serialize(
|
|
ClubStore.BuildMemberListItem(requestedMember, club.clubId)));
|
|
var expectedMemberKeys = new[] { "ClubMemberId", "ClubId", "AccountId", "MembershipType", "CreatedAt" };
|
|
Assert.Equal(expectedMemberKeys.OrderBy(key => key, StringComparer.Ordinal),
|
|
memberJson.RootElement.EnumerateObject().Select(property => property.Name).OrderBy(key => key, StringComparer.Ordinal));
|
|
|
|
var membersAction = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubMembers))!;
|
|
var memberRoutes = membersAction.GetCustomAttributes<HttpGetAttribute>()
|
|
.Select(attribute => attribute.Template);
|
|
Assert.Contains("~/club/{clubId:long}/members", memberRoutes);
|
|
|
|
using var subscription = JsonDocument.Parse(JsonSerializer.Serialize(ClubStore.BuildSubscriptionEnvelope(null, CallerId)));
|
|
Assert.False(subscription.RootElement.GetProperty("success").GetBoolean());
|
|
Assert.Equal(JsonValueKind.Null, subscription.RootElement.GetProperty("value").ValueKind);
|
|
|
|
var accountRoutes = typeof(ClubsAPI).GetMethod(nameof(ClubsAPI.GetClubsForAccount))!
|
|
.GetCustomAttributes(typeof(HttpGetAttribute), false)
|
|
.Cast<HttpGetAttribute>()
|
|
.Select(attribute => attribute.Template);
|
|
Assert.Contains("~/clubs/account/{accountId:long}/clubs", accountRoutes);
|
|
}
|
|
}
|