╭ changed ────────╮ │src/test/undo.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 try std.testing.expectEqualStrings(expected, section.get("value") orelse return error.MissingValue);│ │55 } │ │56 │ │57 -test "repo undo visibility and fresh writer authorization" { │ │57 +test "repo undo visibility and fresh owner authorization" { │ │58 var temp = std.testing.tmpDir(.{}); │ │59 defer temp.cleanup(); │ │60 var arena = std.heap.ArenaAllocator.init(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 session.data.current_page = files; │ │ 96 const page = try ui.Repo.init(&arena, &session, files); │ │ 97 try std.testing.expectEqual(allowed, page.undo != null); │ │ 98 + if (page.undo) |undo| try std.testing.expectEqual(user != null and std.mem.eql(u8, user.?, &owner), undo.can_undo);│ │ 99 var root = ui.Widget{ .repo = try ui.Repo.View.init(allocator, &page, &session) }; │ │100 defer root.deinit(allocator); │ │101 try root.build(allocator, constraint, root.getFocus()); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 _ = try ui.Repo.init(&arena, &session, route); │ │108 } else { │ │109 try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, route)); │ │110 + } │ │111 + if (user == null or !std.mem.eql(u8, user.?, &owner)) { │ │112 try std.testing.expectError(error.Forbidden, Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 2).?.repo_undo));│ │113 } │ │114 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 - var session = try ui.Session.init(&arena, &admin, .{ .user_id = &writer }); │ │115 + var session = try ui.Session.init(&arena, &admin, .{ .user_id = &owner }); │ │116 session.io = io; │ │117 session.repos_dir = repos_dir; │ │118 try Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 2).?.repo_undo);│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 const data = try Undo.init(.{}, &arena, &repo, "alice/demo", null); │ │125 try std.testing.expectEqualStrings("undo", data.items[0].action); │ │126 } │ │124 - // keep the old session/page moment, but revoke its writer's access. │ │125 - repo_event.write_user_ids = ""; │ │127 + // keep the old session/page moment, but transfer ownership to the writer. │ │128 + repo_event.user_id = &writer; │ │129 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }});│ │127 - try std.testing.expectError(error.Forbidden, Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 1).?.repo_undo)); │ │130 + session.next_page = null; │ │131 + Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("alice/demo", 1).?.repo_undo); │ │132 + try std.testing.expectEqualStrings("Forbidden", session.data.undo_failure.?); │ │133 + try std.testing.expectEqual(null, session.next_page); │ │134 │ │135 // public-write repos still require login for history and undo. │ │136 + repo_event.user_id = &owner; │ │137 repo_event.write_access = .public; │ │138 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }});│ │139 var anonymous = try ui.Session.init(&arena, &admin, .{}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │162 try std.testing.expectError(error.NotFound, ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?));│ │163 try std.testing.expectError(error.NotFound, Undo.execute(io, allocator, session.local.?, 1)); │ │164 } else { │ │158 - try Undo.perform(allocator, &session, ui.RoutablePage.repoUndoRoute("", 1).?.repo_undo); │ │165 + Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 999).?.repo_undo); │ │166 + try std.testing.expectEqualStrings("InvalidHistoryIndex", session.data.undo_failure.?); │ │167 + try std.testing.expectEqual(null, session.next_page); │ │168 + Undo.handleRequest(allocator, &session, ui.RoutablePage.repoUndoRoute("", 1).?.repo_undo); │ │169 + try std.testing.expectEqual(null, session.data.undo_failure); │ │170 const refreshed = try ui.Repo.init(&arena, &session, session.next_page.?); │ │171 try std.testing.expectEqualStrings("undo", refreshed.undo.?.items[0].action); │ │172 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │184 defer repo.deinit(io, allocator); │ │185 _ = try repo.commitAtRef(io, allocator, .{ .message = "example commit" }, null, .{ .kind = .head, .name = "master" }); │ │186 const data = try Undo.init(opts, &arena, &repo, "", null); │ │176 - try std.testing.expect(std.mem.startsWith(u8, data.items[0].description, "commit -m \"example commit\"\n\n")); │ │187 + try std.testing.expect(std.mem.startsWith(u8, data.items[0].description, "commit -m \"example commit\"")); │ │188 +} │ │189 + │ │190 +test "malformed undo history is shown as an error" { │ │191 + var temp = std.testing.tmpDir(.{}); │ │192 + defer temp.cleanup(); │ │193 + var arena = std.heap.ArenaAllocator.init(allocator); │ │194 + defer arena.deinit(); │ │195 + const path = try temp.dir.realPathFileAlloc(io, ".", arena.allocator()); │ │196 + const Repo = xit.repo.Repo(.xit, .{}); │ │197 + var repo = try Repo.init(io, allocator, .{ .path = path }); │ │198 + defer repo.deinit(io, allocator); │ │199 + │ │200 + // append an invalid undo record without damaging the repository data. │ │201 + const Corrupt = struct { │ │202 + pub fn run(_: @This(), cursor: *Repo.DB.Cursor(.read_write)) !void { │ │203 + const moment = try Repo.DB.HashMap(.read_write).init(cursor.*); │ │204 + try moment.put(xit.hash.hashInt(.sha1, "undo"), .{ .bytes = "bad" }); │ │205 + } │ │206 + }; │ │207 + const history = try Repo.DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │208 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Corrupt{}); │ │209 + │ │210 + var session = ui.Session{ .arena = &arena, .page_arena = &arena, .io = io, .local = .{ .path = path, .repo_kind = .xit }, .data = .{ .host_kind = .local } };│ │211 + // tabs switch in-page, so every tab carries the undo data and its failure │ │212 + const files = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoFilesRoute("", null, "", "", 0).?); │ │213 + try std.testing.expectEqual(0, files.undo.?.items.len); │ │214 + try std.testing.expectEqualStrings("InvalidUndoRecord", files.undo.?.failure.?); │ │215 + const undo = try ui.Repo.init(&arena, &session, ui.RoutablePage.repoUndoRoute("", null).?); │ │216 + try std.testing.expectEqualStrings("InvalidUndoRecord", undo.undo.?.failure.?); │ │217 + │ │218 + var root = ui.Widget{ .repo_undo = try Undo.View.init(allocator, &undo.undo.?, &session) }; │ │219 + defer root.deinit(allocator); │ │220 + try root.build(allocator, constraint, root.getFocus()); │ │221 + const html = try @import("../web.zig").generateHtml(allocator, &root, &session); │ │222 + defer allocator.free(html); │ │223 + try std.testing.expect(std.mem.indexOf(u8, html, "InvalidUndoRecord") != null); │ │224 } │ │225 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯