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