using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Accounts; using RadicadeServer.API.Notifications; using RadicadeServer.Clubs; using RadicadeServer.Clubs.Announcements; using RadicadeServer.DB; using RadicadeServer.RecNet.Auth; using Xunit; namespace RadicadeServer.Tests; /// /// Player subscriptions (`clubs.rec.net/subscription/*`) and the personal club id they hand back. /// /// The subscribe route only ever existed as a GET, so the client's `POST /subscription/{id}` fell off /// the route table, and `GET /subscription/details/{accountId}` answered `clubId: 0`. That zero is the /// id the client then posts its announcements to, which is why `POST /announcements/club/0` answered /// 404 - the club record it named did not exist. These tests pin the whole loop: subscribe, read the /// personal club id back, and post an announcement to it. /// [Collection(DbFactoryCollection.Name)] public class ClubSubscriptionTests { private const long SubscriberId = 71; private const long CreatorId = 72; private const long StrangerId = 73; private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; public RecDBContext CreateDbContext() => new(options); } private static async Task<(RecDBContext Db, IDbContextFactory Previous)> CreateContextAsync() { var factory = new TestDbContextFactory(); var previous = Notify.DbFactory; Notify.DbFactory = factory; var db = factory.CreateDbContext(); db.Profiles.AddRange(ProfileFor(SubscriberId), ProfileFor(CreatorId), ProfileFor(StrangerId)); await db.SaveChangesAsync(); return (db, previous); } private static Profile ProfileFor(long id) => new() { accountId = id, username = $"player{id}", displayName = $"Player {id}", profileImage = string.Empty, createdAt = DateTime.UtcNow }; private static void SetUser(Controller controller, long userId) { var identity = new ClaimsIdentity("TestAuth"); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.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(Encoding.UTF8.GetBytes(body)); request.ContentLength = request.Body.Length; } private static void SetJsonBody(Controller controller, string body) { var request = controller.HttpContext.Request; request.ContentType = "application/json"; request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body)); request.ContentLength = request.Body.Length; } private static T Property(object? value, string name) { if (value is IDictionary dictionary && dictionary.TryGetValue(name, out var entry)) return (T)entry!; var property = value?.GetType().GetProperty(name); Assert.NotNull(property); return (T)property!.GetValue(value)!; } private static IEnumerable Ids(object? value) => Assert.IsAssignableFrom>(value); [Fact] public async Task SubscribeRecordsTheRelationshipAndReportsTheCreatorsPersonalClub() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var api = new ClubSubscriptionAPI(db); SetUser(api, SubscriberId); var personalClubId = PlayerSubscriptions.PersonalClubId(CreatorId); Assert.True(personalClubId > 0); var subscribed = Assert.IsType(await api.Subscribe(CreatorId)); Assert.Equal(personalClubId, Property(subscribed.Value, "value")); var subscriptions = Assert.IsType(await api.GetMySubscriptions()); Assert.Equal(new[] { CreatorId }, Ids(subscriptions.Value).ToArray()); var details = Assert.IsType(await api.GetAccountSubscriptionDetails(CreatorId)); Assert.Equal(personalClubId, Property(details.Value, "clubId")); Assert.Equal(CreatorId, Property(details.Value, "accountId")); Assert.Equal(1, Property(details.Value, "subscriberCount")); var subscribers = Assert.IsType(await api.GetMySubscribers()); Assert.Empty(Ids(subscribers.Value)); var creatorView = new ClubSubscriptionAPI(db); SetUser(creatorView, CreatorId); var creatorSubscribers = Assert.IsType(await creatorView.GetMySubscribers()); Assert.Equal(new[] { SubscriberId }, Ids(creatorSubscribers.Value).ToArray()); Assert.IsType(await api.Unsubscribe(CreatorId)); Assert.Empty(Ids(Assert.IsType(await api.GetMySubscriptions()).Value)); } finally { Notify.DbFactory = previous; } } [Fact] public async Task BareSubscribeReadsThePlayerIdFromTheBody() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var api = new ClubSubscriptionAPI(db); SetUser(api, SubscriberId); SetJsonBody(api, $"{{\"playerId\":{CreatorId}}}"); var subscribed = Assert.IsType(await api.SubscribeBare()); Assert.Equal(PlayerSubscriptions.PersonalClubId(CreatorId), Property(subscribed.Value, "value")); var subscriptions = Assert.IsType(await api.GetMySubscriptions()); Assert.Equal(new[] { CreatorId }, Ids(subscriptions.Value).ToArray()); } finally { Notify.DbFactory = previous; } } [Fact] public async Task AnnouncementToClubZeroLandsOnTheCallersPersonalClub() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var api = new ClubAnnouncementsAPI(db); SetUser(api, CreatorId); SetFormBody(api, "title=Hello&body=World"); var posted = Assert.IsType(await api.PostClubAnnouncements(0)); var announcementId = Property(posted.Value, "value"); Assert.True(announcementId > 0); var personalClubId = PlayerSubscriptions.PersonalClubId(CreatorId); var feed = Assert.IsType(api.GetClubAnnouncements(personalClubId)); using var document = JsonDocument.Parse(JsonSerializer.Serialize(feed.Value)); var value = document.RootElement.GetProperty("value"); Assert.Equal(personalClubId, value.GetProperty("ClubId").GetInt64()); var announcements = value.GetProperty("Announcements"); Assert.Equal(1, announcements.GetArrayLength()); Assert.Equal(announcementId, announcements[0].GetProperty("AnnouncementId").GetInt64()); Assert.Equal(announcementId, value.GetProperty("LastAnnouncementId").GetInt64()); } finally { Notify.DbFactory = previous; } } [Fact] public async Task AnnouncementCannotBePostedToAnotherAccountsPersonalClub() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var api = new ClubAnnouncementsAPI(db); SetUser(api, StrangerId); SetFormBody(api, "title=Hi&body=There"); var result = await api.PostClubAnnouncements(PlayerSubscriptions.PersonalClubId(CreatorId)); Assert.Equal(StatusCodes.Status403Forbidden, Assert.IsType(result).StatusCode); } finally { Notify.DbFactory = previous; } } [Fact] public async Task BannerImageAcceptsTheNameFromAJsonBody() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var api = new AccountsAPI(db); SetUser(api, CreatorId); SetJsonBody(api, "{\"imageName\":\"banner-abc.jpg\"}"); var result = Assert.IsType(await api.SetBannerImage(null)); Assert.True(Property(result.Value, "success")); Assert.Equal("banner-abc.jpg", (await db.Profiles.AsNoTracking().SingleAsync(x => x.accountId == CreatorId)).bannerImage); } finally { Notify.DbFactory = previous; } } [Fact] public async Task BannerImageWithoutAReadableNameIsStillRefusedAndKeepsTheStoredBanner() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { db.Profiles.Single(x => x.accountId == CreatorId).bannerImage = "keep-me.jpg"; await db.SaveChangesAsync(); var api = new AccountsAPI(db); SetUser(api, CreatorId); SetJsonBody(api, "{}"); var result = await api.SetBannerImage(null); Assert.IsType(result); Assert.Equal("keep-me.jpg", (await db.Profiles.AsNoTracking().SingleAsync(x => x.accountId == CreatorId)).bannerImage); } finally { Notify.DbFactory = previous; } } [Fact] public async Task InvitingAnExistingMemberChangesTheirRoleInsteadOfRefusing() { var (db, previous) = await CreateContextAsync(); await using var disposeDb = db; try { var club = await ClubStore.CreateAsync(db, CreatorId, "Test Club", string.Empty); await ClubStore.InviteMemberAsync(db, club.clubId, StrangerId, ClubMembershipTypes.Member, CreatorId); var accepted = await ClubStore.AcceptInvitationAsync(db, club.clubId, StrangerId); Assert.NotNull(accepted); Assert.Equal(ClubMembershipTypes.Member, ClubStore.GetMembershipType(accepted!, StrangerId)); var api = new ClubsAPI(db); SetUser(api, CreatorId); SetFormBody(api, $"accountId={StrangerId}&membershipType={ClubMembershipTypes.Moderator}"); var result = await api.InviteMember(club.clubId); Assert.IsType(result); var updated = await ClubStore.GetAsync(db, club.clubId); Assert.Equal(ClubMembershipTypes.Moderator, ClubStore.GetMembershipType(updated!, StrangerId)); } finally { Notify.DbFactory = previous; } } }