Data import/export gains the persona's archive (PrivaPub's /clientapi/persona/{id}/archive): made on PrivaPub in
Mastodon's account-archive layout and downloaded through a link; and an archive, its own or Mastodon's, uploaded in
pieces straight from the picked file (js/transfer.js) and imported into the persona, the parts chosen (likes, located
and scheduled posts only when ticked), with progress, a report of what came and what was left out, and Stop. Imported
posts are delivered to no one.
e2e: a town persona's archive downloaded and imported into another, whose profile then shows the posts; the copies are
taken away after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
83 lines
3.6 KiB
C#
83 lines
3.6 KiB
C#
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<ILocator> 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}})");
|
|
}
|
|
}
|
|
}
|
|
}
|