using MongoDB.Bson; using MongoDB.Entities; using PrivaPub.Models.Federation; using PrivaPub.Models.User; using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; using System.Globalization; using System.Net; using System.Text.Json.Nodes; using GroupEntity = PrivaPub.Models.Group.Group; namespace PrivaPub.Tests.Http { [Trait("Category", "Integration")] public sealed class ClientApiPersonasTests : IAsyncLifetime { PrivaPubHost _host; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; static string Name(string prefix) => $"{prefix}{Guid.NewGuid():N}"[..20]; async Task Insert(Root root, object body) { using var client = _host.As(root.Jwt); return await client.PostJson("/clientapi/avatar/private/insert", body); } async Task> Listed(Root root) { using var client = _host.As(root.Jwt); return (await (await client.GetAsync("/clientapi/avatar/private/list", TestContext.Current.CancellationToken)).JsonItems()) .Select(a => a!["id"]!.GetValue()).ToList(); } static DateTime IdDay(string id) => ObjectId.Parse(id).CreationTime; static void WithinTwoWeeksBefore(DateTime created, DateTime published, string id) { Assert.Equal(published.Date, published); Assert.InRange(published, created.Date.AddDays(-13), DateTime.UtcNow.Date); Assert.Equal(published, IdDay(id)); } [Fact] public async Task A_root_id_in_the_body_is_ignored() { var root = await _host.SignUp("owner"); var other = await _host.SignUp("victim"); var userName = Name("planted"); var response = await Insert(root, new { userName, name = "planted", biography = "testing", rootId = other.Id }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var id = (await response.JsonBody())["id"]!.GetValue(); var link = await DB.Default.Find().Match(r => r.AvatarId == id).ExecuteSingleAsync(TestContext.Current.CancellationToken); Assert.Equal(root.Id, link.RootId); Assert.Contains(id, await Listed(root)); Assert.DoesNotContain(id, await Listed(other)); Assert.DoesNotContain(root.Id, await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); } [Theory] [InlineData("Upper")] [InlineData("has-dash")] [InlineData("dot.ted")] [InlineData("spa ce")] [InlineData("ünïcode")] [InlineData("at@sign")] [InlineData("")] public async Task Usernames_outside_the_regex_are_refused(string userName) { var root = await _host.SignUp("regex"); var response = await Insert(root, new { userName, name = "regex", biography = "testing" }); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Contains((await response.JsonBody())["errors"]!.AsObject(), e => e.Key.Equals("userName", StringComparison.OrdinalIgnoreCase)); Assert.Empty(await Listed(root)); if (userName.Length > 0) Assert.False(await DB.Default.Find().Match(r => r.Name == userName || r.Name == userName.ToLowerInvariant()).ExecuteAnyAsync(TestContext.Current.CancellationToken)); } [Theory] [InlineData("admin")] [InlineData("privapub")] [InlineData("root")] [InlineData("moderator")] [InlineData("abuse")] public async Task Reserved_names_are_refused(string userName) { var root = await _host.SignUp("reserved"); var response = await Insert(root, new { userName, name = userName, biography = "testing" }); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Contains("already take", (await response.JsonBody())["errorMessage"]!.GetValue()); Assert.Empty(await Listed(root)); } [Fact] public async Task Personas_and_groups_share_one_name_space() { var root = await _host.SignUp("names"); var persona = await _host.Persona(root, "names"); var group = await _host.Group(persona, community: true); var groupName = group["userName"]!.GetValue(); using var client = _host.As(root.Jwt); var personaOverGroup = await Insert(root, new { userName = groupName, name = "copy", biography = "testing" }); var groupOverPersona = await client.PostJson("/clientapi/group/insert", new { avatarId = persona.Id, userName = persona.UserName, name = "copy", isCommunity = true }); var personaOverPersona = await Insert(await _host.SignUp("names"), new { userName = persona.UserName, name = "copy", biography = "testing" }); Assert.Equal(HttpStatusCode.BadRequest, personaOverGroup.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, groupOverPersona.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, personaOverPersona.StatusCode); var reserved = await DB.Default.Find().Match(r => r.Name == groupName || r.Name == persona.UserName).ExecuteAsync(TestContext.Current.CancellationToken); Assert.Equal(2, reserved.Count); Assert.Contains(reserved, r => r.Name == persona.UserName && r.OwnerKind == LocalActorKind.Person && r.OwnerId == persona.Id); Assert.Contains(reserved, r => r.Name == groupName && r.OwnerKind == LocalActorKind.Group && r.OwnerId == group["id"]!.GetValue()); Assert.Equal(1, await DB.Default.CountAsync(a => a.UserName == persona.UserName, TestContext.Current.CancellationToken)); Assert.Equal(1, await DB.Default.CountAsync(g => g.UserName == groupName, TestContext.Current.CancellationToken)); } [Fact] public async Task An_update_is_delivered_to_followers() { await using var peer = await Peer.Start(); var persona = await _host.Persona(await _host.SignUp("update"), "update"); var follower = new RemoteActor(peer, "fan"); await _host.Follow(follower, peer.A, persona.UserName); var since = DateTime.UtcNow.AddSeconds(-1); using var client = _host.As(persona.Root.Jwt); var response = await client.PostJson("/clientapi/avatar/private/update", new { avatarId = persona.Id, name = "Renamed", biography = "new biography" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var update = Assert.Single(await Jobs.Deliveries(follower.Id + "/inbox", since, TestContext.Current.CancellationToken), d => d["type"]!.GetValue() == "Update"); Assert.Equal($"{PrivaPubHost.Base}/peasants/{persona.UserName}", update["actor"]!.GetValue()); var person = update["object"]!.AsObject(); Assert.Equal("Person", person["type"]!.GetValue()); Assert.Equal($"{PrivaPubHost.Base}/peasants/{persona.UserName}", person["id"]!.GetValue()); Assert.Equal("Renamed", person["name"]!.GetValue()); Assert.DoesNotContain(persona.Root.Id, update.ToJsonString()); Assert.DoesNotContain(persona.Root.UserName, update.ToJsonString()); } [Fact] public async Task Updating_another_roots_persona_is_refused() { var persona = await _host.Persona(await _host.SignUp("mine"), "mine"); var intruder = await _host.SignUp("intruder"); using var client = _host.As(intruder.Jwt); var response = await client.PostJson("/clientapi/avatar/private/update", new { avatarId = persona.Id, name = "Hijacked", biography = "testing" }); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.NotEqual("Hijacked", (await DB.Default.Find().MatchID(persona.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken)).Name); } [Fact] public async Task Published_days_and_ids_fall_within_two_weeks_before_creation() { var created = DateTime.UtcNow; var persona = await _host.Persona(await _host.SignUp("published"), "published"); var group = await _host.Group(persona, community: true); using var client = _host.Client(); using var request = new HttpRequestMessage(HttpMethod.Get, $"/peasants/{persona.UserName}"); request.Headers.Accept.ParseAdd("application/activity+json"); var avatar = await DB.Default.Find().MatchID(persona.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken); var stored = await DB.Default.Find().MatchID(group["id"]!.GetValue()).ExecuteFirstAsync(TestContext.Current.CancellationToken); var actor = JsonNode.Parse(await (await client.SendAsync(request, TestContext.Current.CancellationToken)).Content.ReadAsStringAsync(TestContext.Current.CancellationToken))!; WithinTwoWeeksBefore(created, avatar.PublishedOn, avatar.ID); WithinTwoWeeksBefore(created, stored.PublishedOn, stored.ID); var published = DateTime.Parse(actor["published"]!.GetValue(), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); Assert.Equal(avatar.PublishedOn, published); Assert.DoesNotContain(avatar.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture), actor.ToJsonString()); } [Fact] public async Task The_list_holds_only_the_roots_own_personas() { var root = await _host.SignUp("list"); var other = await _host.SignUp("list"); var first = await _host.Persona(root, "first"); var second = await _host.Persona(root, "second"); var theirs = await _host.Persona(other, "theirs"); Assert.Equal(new[] { first.Id, second.Id }.Order(), (await Listed(root)).Order()); Assert.Equal(new[] { theirs.Id }, await Listed(other)); } } }