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;
}
}
}