using MongoDB.Entities; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Actors; using PrivaPub.Models.Federation; using PrivaPub.Models.Group; using PrivaPub.Models.Post; using System.Net; using System.Net.Http.Json; using System.Text.Json.Nodes; using GroupEntity = PrivaPub.Models.Group.Group; namespace PrivaPub.Tests.Support.Host { public sealed record Fetched(HttpStatusCode Status, string MediaType, string Text, HttpResponseMessage Response) { public JsonObject Json => JsonNode.Parse(Text)!.AsObject(); public string Location => Response.Headers.Location?.OriginalString; } public sealed record LocalGroup(string Id, string UserName) { public string Uri => $"{PrivaPubHost.Base}/peasants/{UserName}"; } public static class FederationHelpers { public const string ActivityJson = "application/activity+json"; public const string LdJson = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; public const string Browser = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; public static string ActorUri(this Persona persona) => $"{PrivaPubHost.Base}/peasants/{persona.UserName}"; public static async Task Fetch(this HttpClient client, string path, string accept = ActivityJson) { using var request = new HttpRequestMessage(HttpMethod.Get, path); if (accept != default) request.Headers.TryAddWithoutValidation("Accept", accept); return await client.Fetch(request); } public static async Task Fetch(this HttpClient client, HttpRequestMessage request) { var response = await client.SendAsync(request, TestContext.Current.CancellationToken); var text = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); return new Fetched(response.StatusCode, response.Content.Headers.ContentType?.MediaType, text, response); } public static async Task Actor(this PrivaPubHost host, Persona persona) => await host.Get().FindById(LocalActorKind.Person, persona.Id, TestContext.Current.CancellationToken); public static async Task Publish(this PrivaPubHost host, Persona persona, StatusDraft draft) { var outcome = await host.Get().Publish(await host.Actor(persona), draft, TestContext.Current.CancellationToken); Assert.True(outcome.Ok, outcome.Error); return outcome.Post; } public static Task Publish(this PrivaPubHost host, Persona persona, string text, PostVisibility visibility = PostVisibility.Public) => host.Publish(persona, new StatusDraft { Text = text, PlainText = true, Visibility = visibility }); public static Task PublishLocated(this PrivaPubHost host, Persona persona, string text) => host.Publish(persona, new StatusDraft { Text = text, PlainText = true, Latitude = 45.46, Longitude = 9.19, RangeKm = 5 }); public static async Task Remove(this PrivaPubHost host, Persona persona, Post post) { var outcome = await host.Get().Remove(await host.Actor(persona), post.ID, TestContext.Current.CancellationToken); Assert.True(outcome.Ok, outcome.Error); } public static async Task Reblog(this PrivaPubHost host, Persona persona, Post original) { var outcome = await host.Get().Reblog(await host.Actor(persona), original.ID, true, PostVisibility.Public, TestContext.Current.CancellationToken); Assert.True(outcome.Ok, outcome.Error); return outcome.Post; } public static async Task FederatedCopy(this PrivaPubHost host, Persona persona) { var post = new Post { GroupUserId = persona.Id, AuthorAccountId = persona.Id, IsFederatedCopy = true, Visibility = PostVisibility.Public, ObjectURI = $"https://remote.example/notes/{Guid.NewGuid():N}", ActorURI = "https://remote.example/users/someone", ContentHtml = "

a copy that is not ours to serve

" }; await DB.Default.SaveAsync(post, TestContext.Current.CancellationToken); return post; } public static async Task Group(this PrivaPubHost host, Persona owner, bool community, string name = default) { var userName = $"{name ?? (community ? "community" : "circle")}{Guid.NewGuid():N}"[..20]; using var client = host.As(owner.Root.Jwt); var response = await client.PostAsJsonAsync("/clientapi/group/insert", new { avatarId = owner.Id, userName, name = userName, description = "testing", isCommunity = community, postingPolicy = community ? "anyone" : default }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var group = (await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken))!; return new LocalGroup(group["id"]!.GetValue(), userName); } public static async Task AddForeignMember(this LocalGroup group, string actorUri) => await DB.Default.Update().MatchID(group.Id) .Modify(b => b.Push(g => g.Members, new GroupMember { AvatarId = actorUri, IsForeign = true })) .ExecuteAsync(TestContext.Current.CancellationToken); public static IEnumerable Nodes(JsonNode node) { if (node == default) yield break; yield return node; switch (node) { case JsonObject obj: foreach (var (_, value) in obj) foreach (var inner in Nodes(value)) yield return inner; break; case JsonArray array: foreach (var item in array) foreach (var inner in Nodes(item)) yield return inner; break; } } public static IEnumerable Keys(JsonNode node) => Nodes(node).OfType().SelectMany(o => o.Select(p => p.Key)); public static IEnumerable Strings(JsonNode node) => Nodes(node).OfType().Where(v => v.TryGetValue(out _)).Select(v => v.GetValue()); public static string PathOf(string url) => new Uri(url).PathAndQuery; } }