using System.IO.Compression; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Actions { // A persona's archive from Settings (owner decision 2026-10-07): one town persona's archive made and downloaded through // its link, then imported into another, whose profile shows the posts while nobody was told; the copies are taken away // again after, so the town stays as seeded public sealed class PersonaArchiveTests(Browser browser) { static async Task Open(IPage page) { await page.GotoAsync($"{Pasture.Site}/settings"); var header = page.Locator(".collapse-title", new() { HasText = "Data import/export" }).First; await header.ClickAsync(); var panel = header.Locator("xpath=..").Locator("[data-archive]"); await panel.WaitForAsync(new() { Timeout = 30000 }); return panel; } [Fact] public async Task An_archive_is_downloaded_and_imported_into_another_persona() { var personas = Pasture.Personas(); var (source, target) = (personas[0], personas[1]); var file = Path.Combine(Path.GetTempPath(), $"decepub-archive-{Guid.NewGuid():N}.zip"); try { await using (var session = await SignIn.As(browser, source)) { var page = session.Page; await Results.Cell("client.action.archive-export", "privapub", source.UserName, async () => { var panel = await Open(page); await panel.Locator("[data-archive-make]").ClickAsync(); var get = panel.Locator("[data-archive-get-link]"); await get.WaitForAsync(new() { Timeout = 120000 }); await get.ClickAsync(); var link = panel.Locator("[data-archive-link]"); var download = await page.RunAndWaitForDownloadAsync(() => link.ClickAsync(), new() { Timeout = 120000 }); await download.SaveAsAsync(file); using var archive = ZipFile.OpenRead(file); Assert.Contains(archive.Entries, e => e.FullName == "outbox.json"); Assert.Contains(archive.Entries, e => e.FullName == "actor.json"); Assert.DoesNotContain(archive.Entries, e => e.FullName.Contains(source.Root, StringComparison.Ordinal)); await session.AssertGuards(); }, page); } await using (var session = await SignIn.As(browser, target)) { var page = session.Page; await Results.Cell("client.action.archive-import", "privapub", target.UserName, async () => { var panel = await Open(page); await panel.Locator("input[data-archive-file]").SetInputFilesAsync(file); foreach (var part in new[] { "follows", "blocks", "mutes", "domainblocks", "lists", "bookmarks", "filters", "tags", "pins" }) await panel.Locator($"[data-archive-part='{part}'] input, input[data-archive-part='{part}']").First.UncheckAsync(); await panel.Locator("[data-archive-import-start]").ClickAsync(); var report = panel.Locator("[data-archive-report][data-archive-state='done']"); await report.WaitForAsync(new() { Timeout = 300000 }); await Assertions.Expect(report.Locator("[data-archive-count='posts']")).ToBeVisibleAsync(); var copies = (await PersonaApi.Call(target, $"/api/v1/accounts/{target.LocalId}/statuses?limit=40"))!.AsArray(); Assert.NotEmpty(copies); await session.AssertGuards(); }, page); } } finally { File.Delete(file); // the copies go, as if never imported: the town stays as seeded await Pasture.Mongo($"const copies = db.Post.find({{GroupUserId:'{target.LocalId}', ImportedFromURI:{{$exists:true, $ne:null}}}}, {{_id:1}}).toArray().map(p => p._id); " + "db.MediaAttachment.deleteMany({PostId:{$in:copies.map(id => id.toString())}}); db.Post.deleteMany({_id:{$in:copies}})"); } } } }