╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ admin.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
8 const fork_repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 9 const author = evt.CommitAuthor{ .name = "haxy", .email = "user@haxy" }; 10 11 test "user and repo" { 12 const io = std.testing.io; 13 const allocator = std.testing.allocator; 14 15 // create the temp dir 16 var temp = std.testing.tmpDir(.{}); 17 defer temp.cleanup(); 18 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 19 defer allocator.free(temp_path); 20 21 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 22 const Repo = rp.Repo(.xit, repo_opts); 23 var repo = try Repo.init(io, allocator, .{ .path = temp_path }); 24 defer repo.deinit(io, allocator); 25 26 var arena = std.heap.ArenaAllocator.init(allocator); 27 defer arena.deinit(); 28 29 // 30 // define test events 31 // 32 33 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 34 const user_event_id = evt.EventWithId.randomId(prng.random()); 35 const repo_event_id = evt.EventWithId.randomId(prng.random()); 36 37 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 38 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 39 40 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 41 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 42 43 const events_to_consume = [_]evt.EventWithId{ 44 .{ 45 .id = std.fmt.bytesToHex(user_event_id, .lower), 46 .author = author, 47 .event = .{ 48 .user = .{ 49 .name = "alice", 50 .email = "alice@example.test", 51 .password_hash = first_password_hash, 52 }, 53 }, 54 }, 55 // this event edits the previous one because it has the same id 56 .{ 57 .id = std.fmt.bytesToHex(user_event_id, .lower), 58 .author = author, 59 .event = .{ 60 .user = .{ 61 .name = "alice", 62 .email = "alice@example.test", 63 .password_hash = second_password_hash, 64 }, 65 }, 66 }, 67 .{ 68 .id = std.fmt.bytesToHex(repo_event_id, .lower), 69 .author = author, 70 .event = .{ 71 .repo = .{ 72 .user_id = &user_event_id, 73 .name = "ziglings", 74 .description = "Learn the Zig programming language by fixing tiny broken programs", 75 }, 76 }, 77 }, 78 }; 79 80 // commit and consume the seed events 81 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 82 83 { 84 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 85 86 // get the map of users 87 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 88 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 89 90 // get the user out of the map that was edited 91 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 92 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 93 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 94 95 // the password was correctly edited 96 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 97 98 // get the map of repos 99 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 100 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 101 102 // get the repo out of the map 103 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 104 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 105 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 106 107 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.event.user_id); 108 try std.testing.expectEqualStrings("ziglings", repo_event.event.name); 109 110 // get the repos created by the user 111 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 112 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 113 114 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 115 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 116 117 try std.testing.expectEqual(1, try user_repos.count()); 118 119 // the set is keyed by orderKeyDesc([created-order][event-id]) 120 const order_key = evt.orderKeyDesc(repo_event.created_order, &repo_event_id); 121 try std.testing.expect(try user_repos.contains(&order_key)); 122 } 123 124 // 125 // remove the repo 126 // 127 128 const events_to_consume2 = [_]evt.EventWithId{ 129 .{ 130 .id = std.fmt.bytesToHex(repo_event_id, .lower), 131 .author = author, 132 .event = .{ .repo = null }, 133 }, 134 }; 135 136 // commit and consume the removal 137 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 138 139 { 140 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 141 142 // get the map of repos 143 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 144 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 145 146 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 147 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 148 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 149 try std.testing.expect(repo_event.removed); 150 151 // get the repos created by the user 152 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 153 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 154 155 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 156 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 157 158 // removing the repo emptied the user's set 159 try std.testing.expectEqual(0, try user_repos.count()); 160 } 161 162 // a non-null payload restores the repo and its indexes 163 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 164 .id = std.fmt.bytesToHex(repo_event_id, .lower), 165 .author = author, 166 .event = events_to_consume[2].event, 167 }}); 168 169 { 170 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 171 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 172 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 173 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 174 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 175 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 176 try std.testing.expect(!repo_event.removed); 177 178 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 179 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 180 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 181 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 182 try std.testing.expectEqual(1, try user_repos.count()); 183 } 184 185 // 186 // remove the user 187 // 188 189 const events_to_consume3 = [_]evt.EventWithId{ 190 .{ 191 .id = std.fmt.bytesToHex(user_event_id, .lower), 192 .author = author, 193 .event = .{ .user = null }, 194 }, 195 }; 196 197 // commit and consume the removal 198 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 199 200 { 201 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 202 203 // get the map of users 204 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 205 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 206 207 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 208 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 209 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 210 try std.testing.expect(user_event.removed); 211 212 // removing the user removes their active repos too 213 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 214 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 215 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 216 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 217 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 218 try std.testing.expect(repo_event.removed); 219 } 220 } 221 222 test "repos and users paginate newest first" { 223 const io = std.testing.io; 224 const allocator = std.testing.allocator; 225 226 var temp = std.testing.tmpDir(.{}); 227 defer temp.cleanup(); 228 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 229 defer allocator.free(temp_path); 230 231 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 232 const Repo = rp.Repo(.xit, repo_opts); 233 var repo = try Repo.init(io, allocator, .{ .path = temp_path }); 234 defer repo.deinit(io, allocator); 235 236 var arena = std.heap.ArenaAllocator.init(allocator); 237 defer arena.deinit(); 238 239 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 240 const user_id = evt.EventWithId.randomId(prng.random()); 241 var repo_ids: [4][evt.event_id_size]u8 = undefined; 242 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 243 244 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 245 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 246 247 // asserts the repo-id-set holds exactly `expected` ids in that order 248 const Check = struct { 249 fn order( 250 comptime DB: type, 251 comptime hash_kind: hash.HashKind, 252 moment: DB.HashMap(.read_only), 253 expected: []const *const [evt.event_id_size]u8, 254 ) !void { 255 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 256 const set = try DB.SortedSet(.read_only).init(cursor); 257 try std.testing.expectEqual(expected.len, try set.count()); 258 for (expected, 0..) |id, i| { 259 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 260 // the key is orderKeyDesc ([created-order][event-id]); its trailing bytes are the id 261 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 262 _ = try kv.key_cursor.readBytes(&order_key); 263 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 264 } 265 } 266 }; 267 268 // one user, then four repos in creation order with deliberately skewed 269 // timestamps 270 const events = [_]evt.EventWithId{ 271 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author = author, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = pw } } }, 272 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 10_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 273 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 10, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 274 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author = author, .timestamp = 5_000, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 275 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author = author, .timestamp = 20, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 276 }; 277 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 278 279 { 280 const moment = try evt.currentMoment(repo_opts, &repo); 281 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 282 283 // the single user shows up in its own ordered set 284 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 285 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 286 try std.testing.expectEqual(1, try uset.count()); 287 } 288 289 // removing repo1 retains its place in the canonical order 290 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 291 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 200, .event = .{ .repo = null } }, 292 }); 293 { 294 const moment = try evt.currentMoment(repo_opts, &repo); 295 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 296 } 297 298 // update repo0 at a later timestamp -> keeps its original place 299 try evt.consume(.admin, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 300 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 301 }); 302 { 303 const moment = try evt.currentMoment(repo_opts, &repo); 304 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 305 306 // the value really was updated 307 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 308 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 309 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 310 const rm = try Repo.DB.HashMap(.read_only).init(rc); 311 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 312 try std.testing.expectEqualStrings("updated", re.event.description); 313 } 314 } 315 316 test "fork query and removal lifecycle" { 317 const io = std.testing.io; 318 const allocator = std.testing.allocator; 319 320 var temp = std.testing.tmpDir(.{}); 321 defer temp.cleanup(); 322 const temp_path = try temp.dir.realPathFileAlloc(io, ".", allocator); 323 defer allocator.free(temp_path); 324 325 const repos_dir = try std.fs.path.join(allocator, &.{ temp_path, "repos" }); 326 defer allocator.free(repos_dir); 327 const admin_path = try std.fs.path.join(allocator, &.{ temp_path, "admin" }); 328 defer allocator.free(admin_path); 329 330 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 331 const user_id = evt.EventWithId.randomId(prng.random()); 332 const repo_id = evt.EventWithId.randomId(prng.random()); 333 const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); 334 const fork_id = evt.EventWithId.randomId(prng.random()); 335 const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); 336 337 var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); 338 defer admin.deinit(io, allocator); 339 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 340 .{ 341 .id = std.fmt.bytesToHex(user_id, .lower), 342 .timestamp = 1, 343 .author = author, 344 .event = .{ .user = .{ 345 .name = author.name, 346 .email = author.email, 347 .password_hash = "", 348 } }, 349 }, 350 .{ 351 .id = std.fmt.bytesToHex(repo_id, .lower), 352 .timestamp = 1, 353 .author = author, 354 .event = .{ .repo = .{ 355 .user_id = &user_id, 356 .name = "repo", 357 .description = "", 358 } }, 359 }, 360 }); 361 362 const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); 363 defer allocator.free(target_path); 364 var target = try rp.Repo(.xit, fork_repo_opts).init(io, allocator, .{ .path = target_path }); 365 defer target.deinit(io, allocator); 366 { 367 const file = try target.core.work_dir.createFile(io, "README", .{}); 368 defer file.close(io); 369 try file.writeStreamingAll(io, "repo\n"); 370 } 371 try target.add(io, allocator, &.{"README"}); 372 _ = try target.commit(io, allocator, .{ .message = "initial" }); 373 374 const draft_path = try fork.create(fork_repo_opts, io, allocator, repos_dir, &admin, .{ 375 .target_branch = "master", 376 .id = fork_id_hex, 377 .user_id = user_id, 378 .repo_id = repo_id, 379 .title = "add a feature", 380 .description = "a draft patch", 381 .tags = "enhancement", 382 .author = author, 383 .timestamp = 2, 384 }); 385 defer allocator.free(draft_path); 386 387 var arena = std.heap.ArenaAllocator.init(allocator); 388 defer arena.deinit(); 389 var moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 390 var record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 391 try std.testing.expectEqualSlices(u8, &user_id, record.event.user_id); 392 try std.testing.expectEqualSlices(u8, &repo_id, record.event.repo_id); 393 try std.testing.expectEqual(.draft, record.event.stage); 394 try std.testing.expectEqual(1, try indexedForkCount(moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); 395 const draft_key = evt.Fork.draftKey(&repo_id, &user_id); 396 try std.testing.expectEqual(1, try indexedForkCount(moment, evt.Fork.repo_user_to_draft_id_set_key, &draft_key)); 397 398 var draft_repo = try rp.Repo(.xit, fork_repo_opts).open(io, allocator, .{ .path = draft_path }); 399 defer draft_repo.deinit(io, allocator); 400 const draft_moment = try evt.currentMoment(fork_repo_opts, &draft_repo); 401 const draft = (try evt.Patch.readById(evt.EventDB(fork_repo_opts.hash), fork_repo_opts.hash, draft_moment, &arena, &fork_id)) orelse return error.NotFound; 402 try std.testing.expectEqualStrings("add a feature", draft.event.title); 403 try std.testing.expectEqual(null, draft.event.revision); 404 405 var wrong_user_id = user_id; 406 wrong_user_id[0] ^= 1; 407 try std.testing.expectError(error.InvalidPatchDraft, fork.remove(io, allocator, repos_dir, &admin, &fork_id_hex, &wrong_user_id, author)); 408 try std.Io.Dir.accessAbsolute(io, draft_path, .{}); 409 410 try fork.remove(io, allocator, repos_dir, &admin, &fork_id_hex, &user_id, author); 411 try std.testing.expectError(error.FileNotFound, std.Io.Dir.accessAbsolute(io, draft_path, .{})); 412 _ = arena.reset(.retain_capacity); 413 moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 414 record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.NotFound; 415 try std.testing.expect(record.removed); 416 try std.testing.expectEqual(0, try indexedForkCount(moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); 417 try std.testing.expectEqual(0, try indexedForkCount(moment, evt.Fork.repo_user_to_draft_id_set_key, &draft_key)); 418 419 try fork.remove(io, allocator, repos_dir, &admin, &fork_id_hex, &user_id, author); 420 } 421 422 fn indexedForkCount( 423 moment: evt.AdminDB.HashMap(.read_only), 424 index_key: []const u8, 425 parent_id: []const u8, 426 ) !u64 { 427 const index_cursor = try moment.getCursor(hash.hashInt(evt.admin_repo_opts.hash, index_key)) orelse return 0; 428 const index = try evt.AdminDB.HashMap(.read_only).init(index_cursor); 429 const ids_cursor = try index.getCursor(hash.hashInt(evt.admin_repo_opts.hash, parent_id)) orelse return 0; 430 const ids = try evt.AdminDB.SortedSet(.read_only).init(ids_cursor); 431 return try ids.count(); 432 } 433