using MongoDB.Entities; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Objects; using PrivaPub.Models.Jobs; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Federation { [Trait("Category", "Integration")] public sealed class PollTests : IAsyncLifetime { Harness _harness; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _harness = await Harness.Start(); } public async ValueTask DisposeAsync() { if (_harness != default) await _harness.DisposeAsync(); } static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority); static string NewId(RemoteActor actor, string kind) => $"{Origin(actor)}/{kind}/{Guid.NewGuid():N}"; async Task Follows(RemoteActor follower, string target) => await _harness.Deliver(follower, "/human-centipede", new JsonObject { ["id"] = NewId(follower, "follows"), ["type"] = "Follow", ["actor"] = follower.Id, ["object"] = target }); JsonObject Vote(RemoteActor voter, string question, string owner, string choice) => new() { ["id"] = NewId(voter, "activities"), ["type"] = "Create", ["actor"] = voter.Id, ["to"] = new JsonArray(owner), ["object"] = new JsonObject { ["id"] = NewId(voter, "votes"), ["type"] = "Note", ["name"] = choice, ["attributedTo"] = voter.Id, ["inReplyTo"] = question, ["to"] = new JsonArray(owner) } }; [Fact] public async Task A_local_poll_goes_out_as_a_question_and_counts_each_remote_voter_once() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var mallory = new RemoteActor(_harness.Peer, "mallory"); await Follows(mallory, alice.Uri); var outcome = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "which?", Poll = new PollDraft(new[] { "tea", "coffee" }, 3600, false, false) }, token); Assert.True(outcome.Ok); var question = (await _harness.Outgoing(mallory.Id + "/inbox")).Single(a => a["type"]!.GetValue() == "Create")["object"]!; Assert.Equal("Question", question["type"]!.GetValue()); Assert.Equal(new[] { "tea", "coffee" }, question["oneOf"]!.AsArray().Select(o => o!["name"]!.GetValue())); await _harness.Deliver(mallory, "/human-centipede", Vote(mallory, outcome.Post.ObjectURI, alice.Uri, "coffee")); await _harness.Deliver(mallory, "/human-centipede", Vote(mallory, outcome.Post.ObjectURI, alice.Uri, "tea")); var poll = (await DB.Default.Find().OneAsync(outcome.Post.ID, token)).Poll; Assert.Equal(new[] { 0, 1 }, poll.Options.Select(o => o.Votes)); Assert.Equal(1, poll.VotersCount); Assert.False(await DB.Default.Find().Match(p => p.InReplyToURI == outcome.Post.ObjectURI).ExecuteAnyAsync(token)); Assert.True(await DB.Default.Find().Match(j => j.Kind == JobKind.PollRefresh && j.Payload == outcome.Post.ID).ExecuteAnyAsync(token)); Assert.True(await DB.Default.Find().Match(j => j.Kind == JobKind.PollClose && j.Payload == outcome.Post.ID).ExecuteAnyAsync(token)); } [Fact] public async Task A_persona_votes_on_a_remote_poll_and_a_count_refresh_is_not_an_edit() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var mallory = new RemoteActor(_harness.Peer, "mallory"); var questionId = NewId(mallory, "questions"); JsonObject Question(int first, int second) => new() { ["id"] = questionId, ["type"] = "Question", ["attributedTo"] = mallory.Id, ["content"] = "

pick

", ["to"] = new JsonArray(Addressing.Public), ["cc"] = new JsonArray(alice.Uri), ["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@alice" }), ["endTime"] = DateTime.UtcNow.AddDays(1).ToString("O"), ["anyOf"] = new JsonArray( new JsonObject { ["type"] = "Note", ["name"] = "red", ["replies"] = new JsonObject { ["type"] = "Collection", ["totalItems"] = first } }, new JsonObject { ["type"] = "Note", ["name"] = "blue", ["replies"] = new JsonObject { ["type"] = "Collection", ["totalItems"] = second } }) }; await _harness.Deliver(mallory, "/human-centipede", new JsonObject { ["id"] = NewId(mallory, "activities"), ["type"] = "Create", ["actor"] = mallory.Id, ["object"] = Question(3, 1) }); var post = await DB.Default.Find().Match(p => p.ObjectURI == questionId).ExecuteFirstAsync(token); Assert.True(post.Poll.Multiple); Assert.True((await _harness.Polls.Vote(alice, post, new[] { 0, 1 }, token)).Ok); Assert.False((await _harness.Polls.Vote(alice, post, new[] { 0 }, token)).Ok); var votes = (await _harness.Outgoing(mallory.Id + "/inbox")).Where(a => a["type"]!.GetValue() == "Create").Select(a => a["object"]!).ToList(); Assert.Equal(new[] { "red", "blue" }, votes.Select(v => v["name"]!.GetValue()).Order().Reverse()); Assert.All(votes, v => Assert.Equal(questionId, v["inReplyTo"]!.GetValue())); Assert.All(votes, v => Assert.Null(v["published"])); Assert.Equal(new[] { 0, 1 }, (await _harness.Polls.OwnVotes(post.ID, alice.Id, token)).Order()); await _harness.Deliver(mallory, "/human-centipede", new JsonObject { ["id"] = NewId(mallory, "activities"), ["type"] = "Update", ["actor"] = mallory.Id, ["object"] = Question(9, 5) }); var refreshed = await DB.Default.Find().OneAsync(post.ID, token); Assert.Equal(new[] { 9, 5 }, refreshed.Poll.Options.Select(o => o.Votes)); Assert.Empty(refreshed.Revisions); Assert.Null(refreshed.EditedAt); } [Fact] public async Task Closing_a_poll_tells_its_voters_and_its_author() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var (_, bob) = await _harness.Persona("bob"); var outcome = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "quick", Poll = new PollDraft(new[] { "yes", "no" }, 300, false, false) }, token); Assert.True((await _harness.Polls.Vote(bob, outcome.Post, new[] { 0 }, token)).Ok); Assert.False((await _harness.Polls.Vote(alice, outcome.Post, new[] { 0 }, token)).Ok); var job = await DB.Default.Find().Match(j => j.Kind == JobKind.PollClose && j.Payload == outcome.Post.ID).ExecuteFirstAsync(token); await new PollCloseJob(_harness.Db, _harness.Local, _harness.Outbox).Handle(job, token); Assert.NotNull((await DB.Default.Find().OneAsync(outcome.Post.ID, token)).Poll.ClosedAt); var told = await DB.Default.Find().Match(n => n.Type == NotificationType.Poll && n.PostId == outcome.Post.ID).ExecuteAsync(token); Assert.Equal(new[] { alice.Id, bob.Id }.Order(), told.Select(n => n.AvatarId).Order()); Assert.False((await _harness.Polls.Vote(bob, await DB.Default.Find().OneAsync(outcome.Post.ID, token), new[] { 1 }, token)).Ok); } } }