138 lines
6.5 KiB
C#
138 lines
6.5 KiB
C#
using System.Formats.Tar;
|
|||
|
|
using System.Text.Json.Nodes;
|
||
|
|
|
||
|
|
using decePubClient.E2E.Support;
|
||
|
|
|
||
|
|
using Microsoft.Playwright;
|
||
|
|
|
||
|
|
namespace decePubClient.E2E.Actions
|
||
|
|
{
|
||
|
|
// The server's backups from Administration (owner decision 2026-10-07), as the pasture's administrator: one made now,
|
||
|
|
// downloaded through its link (a tar whose files are the backup's), deleted and uploaded again from the browser in
|
||
|
|
// pieces; each step checked on /clientapi
|
||
|
|
public sealed class BackupsTests(Browser browser)
|
||
|
|
{
|
||
|
|
const string Root = PastureAdmin.Root;
|
||
|
|
static readonly Pasture.Persona Admin = new("alice", "alice", Root, PastureAdmin.Password, null, null, null);
|
||
|
|
|
||
|
|
internal static async Task<ILocator> Open(IPage page)
|
||
|
|
{
|
||
|
|
await page.GotoAsync($"{Pasture.Site}/administration");
|
||
|
|
var header = page.Locator(".collapse-title", new() { HasText = "Backups" }).First;
|
||
|
|
await header.ClickAsync();
|
||
|
|
var panel = header.Locator("xpath=..").Locator("[data-backups]");
|
||
|
|
await panel.Locator("[data-backup-now]").WaitForAsync(new() { Timeout = 30000 });
|
||
|
|
return panel;
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static async Task<string> BackUp(ILocator panel)
|
||
|
|
{
|
||
|
|
var before = (await PastureAdmin.Get("/clientapi/admin/backups"))!["backups"]!.AsArray().Select(b => b!["id"]!.GetValue<string>()).ToHashSet();
|
||
|
|
await panel.Locator("[data-backup-now]").ClickAsync();
|
||
|
|
var made = default(string);
|
||
|
|
Assert.True(await Pasture.Eventually(async () =>
|
||
|
|
{
|
||
|
|
var list = (await PastureAdmin.Get("/clientapi/admin/backups"))!;
|
||
|
|
made = list["backups"]!.AsArray().Select(b => b!["id"]!.GetValue<string>()).FirstOrDefault(id => !before.Contains(id));
|
||
|
|
return made is not null && list["running"] is null;
|
||
|
|
}, 300), "the backup was never made");
|
||
|
|
await Assertions.Expect(panel.Locator($"[data-backup='{made}']")).ToBeVisibleAsync(new() { Timeout = 30000 });
|
||
|
|
return made;
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_backup_is_made_downloaded_and_uploaded_again()
|
||
|
|
{
|
||
|
|
await using var session = await SignIn.As(browser, Admin);
|
||
|
|
var page = session.Page;
|
||
|
|
var file = Path.Combine(Path.GetTempPath(), $"decepub-backup-{Guid.NewGuid():N}.tar");
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await Results.Cell("client.admin.backup", "privapub", Root, async () =>
|
||
|
|
{
|
||
|
|
var panel = await Open(page);
|
||
|
|
var id = await BackUp(panel);
|
||
|
|
var row = panel.Locator($"[data-backup='{id}']");
|
||
|
|
await Assertions.Expect(row).ToContainTextAsync("Made by hand");
|
||
|
|
|
||
|
|
// the link, for the password
|
||
|
|
await row.Locator("[data-backup-download]").ClickAsync();
|
||
|
|
var dialog = page.Locator("[data-backup-download-dialog]");
|
||
|
|
await dialog.Locator("[data-backup-password]").FillAsync(PastureAdmin.Password);
|
||
|
|
await dialog.Locator("[data-backup-confirm]").ClickAsync();
|
||
|
|
var link = dialog.Locator("[data-backup-link]");
|
||
|
|
await Assertions.Expect(link).ToBeVisibleAsync(new() { Timeout = 15000 });
|
||
|
|
var download = await page.RunAndWaitForDownloadAsync(() => link.ClickAsync(), new() { Timeout = 120000 });
|
||
|
|
await download.SaveAsAsync(file);
|
||
|
|
var names = new List<string>();
|
||
|
|
await using (var reader = new TarReader(File.OpenRead(file)))
|
||
|
|
while (await reader.GetNextEntryAsync() is { } entry)
|
||
|
|
names.Add(entry.Name);
|
||
|
|
Assert.Contains($"{id}/manifest.json", names);
|
||
|
|
Assert.Contains(names, n => n.StartsWith($"{id}/db/", StringComparison.Ordinal));
|
||
|
|
await dialog.Locator("[data-backup-dialog-close]").ClickAsync();
|
||
|
|
|
||
|
|
// deleted (asked first), then uploaded again from the file
|
||
|
|
await row.Locator("[data-backup-delete]").ClickAsync();
|
||
|
|
await page.Locator("[data-confirm-yes]").ClickAsync();
|
||
|
|
await Assertions.Expect(row).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
||
|
|
Assert.DoesNotContain((await PastureAdmin.Get("/clientapi/admin/backups"))!["backups"]!.AsArray(), b => b!["id"]!.GetValue<string>() == id);
|
||
|
|
|
||
|
|
await panel.Locator("input[data-backup-upload]").SetInputFilesAsync(file);
|
||
|
|
await panel.Locator("[data-backup-upload-start]").ClickAsync();
|
||
|
|
await Assertions.Expect(row).ToContainTextAsync("Uploaded", new() { Timeout = 120000 });
|
||
|
|
var uploaded = (await PastureAdmin.Get("/clientapi/admin/backups"))!["backups"]!.AsArray().Single(b => b!["id"]!.GetValue<string>() == id)!;
|
||
|
|
Assert.Equal("uploaded", uploaded["kind"]!.GetValue<string>());
|
||
|
|
await session.AssertGuards();
|
||
|
|
}, page);
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
File.Delete(file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A real restore of the pasture from Administration: PrivaPub stops and restores as it starts again, every session
|
||
|
|
// ends, and the page says so; signed in again, the last restore's report is there. It restores the whole server and
|
||
|
|
// ends the town's sessions, so it runs only when asked (--filter "Category=Restore"), alone, and signs the town in
|
||
|
|
// again (town.sh renew privapub).
|
||
|
|
[Trait("Category", "Restore")]
|
||
|
|
public sealed class RestoreTests(Browser browser)
|
||
|
|
{
|
||
|
|
static readonly Pasture.Persona Admin = new("alice", "alice", PastureAdmin.Root, PastureAdmin.Password, null, null, null);
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_backup_is_restored_and_everyone_signs_in_again()
|
||
|
|
{
|
||
|
|
await using var session = await SignIn.As(browser, Admin);
|
||
|
|
var page = session.Page;
|
||
|
|
await Results.Cell("client.admin.restore", "privapub", PastureAdmin.Root, async () =>
|
||
|
|
{
|
||
|
|
var panel = await BackupsTests.Open(page);
|
||
|
|
var id = await BackupsTests.BackUp(panel);
|
||
|
|
var host = (await PastureAdmin.Get("/clientapi/admin/backups"))!["host"]!.GetValue<string>();
|
||
|
|
|
||
|
|
await panel.Locator($"[data-backup='{id}'] [data-backup-restore]").ClickAsync();
|
||
|
|
var dialog = page.Locator("[data-backup-restore-dialog]");
|
||
|
|
await dialog.Locator("[data-backup-password]").FillAsync(PastureAdmin.Password);
|
||
|
|
var confirm = dialog.Locator("[data-backup-confirm]");
|
||
|
|
await dialog.Locator("[data-backup-host]").FillAsync("elsewhere.test");
|
||
|
|
await Assertions.Expect(confirm).ToBeDisabledAsync();
|
||
|
|
await dialog.Locator("[data-backup-host]").FillAsync(host);
|
||
|
|
await confirm.ClickAsync();
|
||
|
|
await Assertions.Expect(page.Locator("[data-backup-restarting]")).ToBeVisibleAsync(new() { Timeout = 15000 });
|
||
|
|
|
||
|
|
// once PrivaPub is back, the session it ended sends the page to sign in again
|
||
|
|
await page.WaitForURLAsync(url => url.Contains("/login"), new() { Timeout = 300000 });
|
||
|
|
await Pasture.RenewTown();
|
||
|
|
}, page);
|
||
|
|
|
||
|
|
await using var again = await SignIn.As(browser, Admin);
|
||
|
|
var panelAgain = await BackupsTests.Open(again.Page);
|
||
|
|
await Assertions.Expect(panelAgain.Locator("[data-last-restore]")).ToBeVisibleAsync(new() { Timeout = 30000 });
|
||
|
|
Assert.NotNull((await PastureAdmin.Get("/clientapi/admin/backups"))!["lastRestore"]);
|
||
|
|
await again.AssertGuards();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|