fixtures.ts
1 /* eslint-disable @typescript-eslint/naming-convention */ 2 import type { Config } from "@http-client"; 3 import type { PeerManager, RadiclePeer } from "./peerManager.js"; 4 import type * as Stream from "node:stream"; 5 6 import * as Fs from "node:fs/promises"; 7 import * as Path from "node:path"; 8 import { test as base, expect } from "@playwright/test"; 9 import { execa } from "execa"; 10 11 import * as issue from "@tests/support/cobs/issue.js"; 12 import * as logLabel from "@tests/support/logPrefix.js"; 13 import * as patch from "@tests/support/cobs/patch.js"; 14 import { createOptions, supportDir, tmpDir } from "@tests/support/support.js"; 15 import { createPeerManager } from "@tests/support/peerManager.js"; 16 import { createRepo } from "@tests/support/repo.js"; 17 import { formatCommit } from "@app/lib/utils.js"; 18 19 export { expect }; 20 21 const fixturesDir = Path.resolve(supportDir, "..", "./fixtures"); 22 23 export const test = base.extend<{ 24 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 25 forAllTests: void; 26 stateDir: string; 27 peerManager: PeerManager; 28 peer: RadiclePeer; 29 outputLog: Stream.Writable; 30 }>({ 31 forAllTests: [ 32 async ({ outputLog, page }, use) => { 33 const browserLabel = logLabel.logPrefix("browser"); 34 page.on("console", msg => { 35 // Ignore common console logs that we don't care about. 36 if ( 37 msg.text().startsWith("[vite] connected.") || 38 msg.text().startsWith("[vite] connecting...") || 39 msg.text().startsWith("Not able to parse url") || 40 msg 41 .text() 42 .includes("Please make sure it wasn't preloaded for nothing.") 43 ) { 44 return; 45 } 46 log(msg.text(), browserLabel, outputLog); 47 }); 48 49 if (!process.env.CONTINUE_ON_ERRORS) { 50 page.on("pageerror", msg => { 51 expect( 52 false, 53 `Test failed because there was a console error in the app: ${msg}`, 54 ).toBeTruthy(); 55 }); 56 } 57 58 const playwrightLabel = logLabel.logPrefix("playwright"); 59 60 function isLocalhost(url: URL) { 61 return url.hostname === "localhost" || url.hostname === "127.0.0.1"; 62 } 63 64 await page.route( 65 url => !isLocalhost(url), 66 route => { 67 log( 68 `Aborted remote request: ${route.request().url()}`, 69 playwrightLabel, 70 outputLog, 71 ); 72 return route.abort(); 73 }, 74 ); 75 76 await page.route( 77 url => 78 url.href.startsWith("https://www.gravatar.com/avatar/") || 79 (url.href.endsWith(".png") && !isLocalhost(url)), 80 route => { 81 return route.fulfill({ 82 status: 200, 83 path: "./public/radicle.svg", 84 }); 85 }, 86 ); 87 88 await use(); 89 }, 90 { scope: "test", auto: true }, 91 ], 92 93 outputLog: async ({ stateDir }, use) => { 94 const logFile = await Fs.open(Path.join(stateDir, "test.log"), "a"); 95 await use(logFile.createWriteStream()); 96 await logFile.close(); 97 }, 98 99 peerManager: async ({ stateDir, outputLog }, use) => { 100 const peerManager = await createPeerManager({ 101 dataDir: Path.resolve(Path.join(stateDir, "peers")), 102 outputLog, 103 }); 104 await use(peerManager); 105 await peerManager.shutdown(); 106 }, 107 108 peer: async ({ peerManager }, use) => { 109 const peer = await peerManager.createPeer({ 110 name: "httpd", 111 gitOptions: gitOptions["bob"], 112 }); 113 114 await peer.startNode(); 115 await peer.startHttpd(); 116 117 await use(peer); 118 }, 119 120 // eslint-disable-next-line no-empty-pattern 121 stateDir: async ({}, use, testInfo) => { 122 const stateDir = testInfo.outputDir; 123 await Fs.rm(stateDir, { recursive: true, force: true }); 124 await Fs.mkdir(stateDir, { recursive: true }); 125 126 await use(stateDir); 127 if ( 128 process.env.CI && 129 (testInfo.status === "passed" || testInfo.status === "skipped") 130 ) { 131 await Fs.rm(stateDir, { recursive: true }); 132 } 133 }, 134 }); 135 136 function log(text: string, label: string, outputLog: Stream.Writable) { 137 const output = text 138 .split("\n") 139 .map(line => `${label}${line}`) 140 .join("\n"); 141 142 outputLog.write(`${output}\n`); 143 if (!process.env.CI) { 144 console.log(output); 145 } 146 } 147 148 export async function createSourceBrowsingFixture( 149 peerManager: PeerManager, 150 palm: RadiclePeer, 151 ) { 152 const repoName = "source-browsing"; 153 const sourceBrowsingDir = Path.join(tmpDir, "repos", repoName); 154 await Fs.mkdir(sourceBrowsingDir, { recursive: true }); 155 await execa("tar", [ 156 "-xf", 157 Path.join(fixturesDir, `repos/${repoName}.tar.bz2`), 158 "-C", 159 sourceBrowsingDir, 160 ]); 161 const rid = sourceBrowsingRid; 162 const alice = await peerManager.createPeer({ 163 name: "alice", 164 gitOptions: gitOptions["alice"], 165 }); 166 const aliceRepoPath = Path.join(alice.checkoutPath, "source-browsing"); 167 const bob = await peerManager.createPeer({ 168 name: "bob", 169 gitOptions: gitOptions["bob"], 170 }); 171 const bobRepoPath = Path.join(bob.checkoutPath, "source-browsing"); 172 await alice.startNode({ 173 node: { 174 ...defaultConfig.node, 175 connect: [palm.address], 176 alias: "alice", 177 }, 178 }); 179 await bob.startNode({ 180 node: { ...defaultConfig.node, connect: [palm.address], alias: "bob" }, 181 }); 182 await palm.waitForEvent({ type: "peerConnected", nid: alice.nodeId }, 1000); 183 await palm.waitForEvent({ type: "peerConnected", nid: bob.nodeId }, 1000); 184 185 await alice.git(["clone", sourceBrowsingDir], { cwd: alice.checkoutPath }); 186 await alice.git(["checkout", "feature/branch"], { cwd: aliceRepoPath }); 187 await alice.git(["checkout", "feature/move-copy-files"], { 188 cwd: aliceRepoPath, 189 }); 190 await alice.git(["checkout", "orphaned-branch"], { cwd: aliceRepoPath }); 191 await alice.git(["checkout", "main"], { cwd: aliceRepoPath }); 192 await alice.rad( 193 [ 194 "init", 195 "--name", 196 repoName, 197 "--default-branch", 198 "main", 199 "--description", 200 "Git repository for source browsing tests", 201 "--public", 202 ], 203 { cwd: aliceRepoPath }, 204 ); 205 await alice.waitForEvent( 206 { 207 type: "seedDiscovered", 208 rid, 209 nid: palm.nodeId, 210 }, 211 2000, 212 ); 213 214 // Needed due to rad init not pushing all branches. 215 await alice.git(["push", "rad", "--all"], { cwd: aliceRepoPath }); 216 await alice.stopNode(); 217 218 await bob.waitForEvent( 219 { 220 type: "seedDiscovered", 221 rid, 222 nid: palm.nodeId, 223 }, 224 2000, 225 ); 226 227 await bob.rad(["clone", rid], { cwd: bob.checkoutPath }); 228 229 await Fs.writeFile( 230 Path.join(bob.checkoutPath, "source-browsing", "README.md"), 231 "Updated readme", 232 ); 233 await bob.git(["add", "README.md"], { cwd: bobRepoPath }); 234 await bob.git( 235 [ 236 "commit", 237 "--message", 238 "Update readme", 239 "--date", 240 "Mon Dec 21 14:00 2022 +0100", 241 ], 242 { cwd: bobRepoPath }, 243 ); 244 await bob.git(["push", "rad"], { cwd: bobRepoPath }); 245 await bob.stopNode(); 246 } 247 248 export async function createCobsFixture( 249 peerManager: PeerManager, 250 peer: RadiclePeer, 251 ) { 252 await peer.rad(["follow", peer.nodeId, "--alias", "palm"]); 253 await Fs.mkdir(Path.join(tmpDir, "repos", "cobs")); 254 const { repoFolder, rid, defaultBranch } = await createRepo(peer, { 255 name: "cobs", 256 }); 257 const eve = await peerManager.createPeer({ 258 name: "eve", 259 gitOptions: gitOptions["eve"], 260 }); 261 await eve.startNode({ 262 node: { ...defaultConfig.node, connect: [peer.address], alias: "eve" }, 263 }); 264 await eve.rad(["clone", rid], { cwd: eve.checkoutPath }); 265 266 const issueOne = await issue.create( 267 peer, 268 "This `title` has **markdown**", 269 "This is a description\nWith some multiline text.", 270 ["bug", "feature-request"], 271 { cwd: repoFolder }, 272 ); 273 await peer.rad( 274 ["issue", "react", issueOne, "--emoji", "👍", "--to", issueOne], 275 { 276 cwd: repoFolder, 277 }, 278 ); 279 await peer.rad( 280 ["issue", "react", issueOne, "--emoji", "🎉", "--to", issueOne], 281 { 282 cwd: repoFolder, 283 }, 284 ); 285 await peer.rad( 286 ["issue", "assign", issueOne, "--add", `did:key:${peer.nodeId}`], 287 createOptions(repoFolder, 1), 288 ); 289 const { stdout: commentIssueOne } = await peer.rad( 290 [ 291 "issue", 292 "comment", 293 issueOne, 294 "--message", 295 "This is a multiline comment\n\nWith some more text.", 296 "--quiet", 297 "--no-announce", 298 ], 299 createOptions(repoFolder, 2), 300 ); 301 await peer.rad( 302 ["issue", "react", issueOne, "--emoji", "🙏", "--to", commentIssueOne], 303 { 304 cwd: repoFolder, 305 }, 306 ); 307 const { stdout: replyIssueOne } = await peer.rad( 308 [ 309 "issue", 310 "comment", 311 issueOne, 312 "--message", 313 "This is a reply, to a first comment.", 314 "--reply-to", 315 commentIssueOne, 316 "--quiet", 317 "--no-announce", 318 ], 319 createOptions(repoFolder, 3), 320 ); 321 await peer.rad( 322 ["issue", "react", issueOne, "--emoji", "🚀", "--to", replyIssueOne], 323 { 324 cwd: repoFolder, 325 }, 326 ); 327 await peer.rad( 328 [ 329 "issue", 330 "comment", 331 issueOne, 332 "--message", 333 "A root level comment after a reply, for margins sake.", 334 "--quiet", 335 "--no-announce", 336 ], 337 createOptions(repoFolder, 4), 338 ); 339 340 const issueTwo = await issue.create( 341 peer, 342 "A closed issue", 343 "This issue has been closed\n\nsource: [link](https://radicle.xyz)", 344 [], 345 { cwd: repoFolder }, 346 ); 347 await peer.rad( 348 ["issue", "state", issueTwo, "--closed"], 349 createOptions(repoFolder, 1), 350 ); 351 352 const issueThree = await issue.create( 353 peer, 354 "A solved issue", 355 "This issue has been solved\n\n```js\nconsole.log('hello world')\nconsole.log(\"\")\n```", 356 [], 357 { cwd: repoFolder }, 358 ); 359 await peer.rad( 360 ["issue", "state", issueThree, "--solved"], 361 createOptions(repoFolder, 1), 362 ); 363 364 const patchOne = await patch.create( 365 peer, 366 ["Add README", "This commit adds more information to the README"], 367 "feature/add-readme", 368 () => Fs.writeFile(Path.join(repoFolder, "README.md"), "# Cobs Repo"), 369 ["Let's add a README", "This repo needed a README"], 370 { cwd: repoFolder }, 371 ); 372 const { stdout: commentPatchOne } = await peer.rad( 373 [ 374 "patch", 375 "comment", 376 patchOne, 377 "--message", 378 "I'll review the patch", 379 "--quiet", 380 "--no-announce", 381 ], 382 createOptions(repoFolder, 1), 383 ); 384 await peer.rad( 385 [ 386 "patch", 387 "comment", 388 patchOne, 389 "--message", 390 "Thanks for that!", 391 "--reply-to", 392 commentPatchOne, 393 "--quiet", 394 "--no-announce", 395 ], 396 createOptions(repoFolder, 2), 397 ); 398 await peer.rad( 399 [ 400 "patch", 401 "comment", 402 patchOne, 403 "--message", 404 "Yeah no problem!", 405 "--reply-to", 406 commentPatchOne, 407 "--quiet", 408 "--no-announce", 409 ], 410 createOptions(repoFolder, 3), 411 ); 412 const { stdout: commentTwo } = await peer.rad( 413 [ 414 "patch", 415 "comment", 416 patchOne, 417 "--message", 418 "Looking good so far", 419 "--quiet", 420 "--no-announce", 421 ], 422 createOptions(repoFolder, 4), 423 ); 424 await peer.rad( 425 [ 426 "patch", 427 "comment", 428 patchOne, 429 "--message", 430 "Thanks again!", 431 "--reply-to", 432 commentTwo, 433 "--quiet", 434 "--no-announce", 435 ], 436 createOptions(repoFolder, 5), 437 ); 438 await peer.rad( 439 ["patch", "review", patchOne, "-m", "LGTM", "--accept"], 440 createOptions(repoFolder, 6), 441 ); 442 await patch.merge( 443 peer, 444 defaultBranch, 445 "feature/add-readme", 446 createOptions(repoFolder, 7), 447 ); 448 449 const patchTwo = await patch.create( 450 peer, 451 ["Add subtitle to README"], 452 "feature/add-more-text", 453 () => Fs.appendFile(Path.join(repoFolder, "README.md"), "\n\n## Subtitle"), 454 [], 455 { cwd: repoFolder }, 456 ); 457 await peer.rad( 458 [ 459 "patch", 460 "review", 461 patchTwo, 462 "-m", 463 "Not the README we are looking for", 464 "--reject", 465 ], 466 createOptions(repoFolder, 1), 467 ); 468 469 const patchThree = await patch.create( 470 peer, 471 [ 472 "Rewrite subtitle to README", 473 "This was really necessary", 474 "Blazingly fast", 475 ], 476 "feature/better-subtitle", 477 () => Fs.appendFile(Path.join(repoFolder, "README.md"), "\n\n## Better?"), 478 [ 479 "Taking another stab at the README", 480 "This is a big improvement over the last one", 481 "Hopefully **this** is the last time", 482 ], 483 { cwd: repoFolder }, 484 ); 485 await peer.rad( 486 ["patch", "label", patchThree, "--add", "documentation"], 487 createOptions(repoFolder, 1), 488 ); 489 await eve.rad( 490 ["patch", "review", patchThree, "-m", "This looks better"], 491 createOptions(repoFolder, 2), 492 ); 493 await Fs.appendFile( 494 Path.join(repoFolder, "README.md"), 495 "\n\nHad to push a new revision", 496 ); 497 await peer.git(["add", "."], { cwd: repoFolder }); 498 await peer.git(["commit", "-m", "Add more text"], { cwd: repoFolder }); 499 await peer.git( 500 [ 501 "push", 502 "-o", 503 "patch.message=Most of the missing README text was caused by the git-daemon not having a writers block. It seems like using an RNG was not a good enough solution.", 504 "-o", 505 "patch.message=After this change, the README seem to be written correctly", 506 "rad", 507 "feature/better-subtitle", 508 ], 509 createOptions(repoFolder, 3), 510 ); 511 await peer.rad( 512 [ 513 "patch", 514 "review", 515 patchThree, 516 "-m", 517 "No this doesn't look better", 518 "--reject", 519 ], 520 createOptions(repoFolder, 2), 521 ); 522 523 const patchFour = await patch.create( 524 peer, 525 ["This patch is going to be archived"], 526 "feature/archived", 527 () => Fs.writeFile(Path.join(repoFolder, "CONTRIBUTING.md"), "# Archived"), 528 [], 529 { cwd: repoFolder }, 530 ); 531 await peer.rad( 532 [ 533 "patch", 534 "review", 535 patchFour, 536 "-m", 537 "No review due to patch being archived.", 538 ], 539 createOptions(repoFolder, 1), 540 ); 541 await peer.rad(["patch", "archive", patchFour], createOptions(repoFolder, 2)); 542 543 const patchFive = await patch.create( 544 peer, 545 ["This patch is going to be reverted to draft"], 546 "feature/draft", 547 () => Fs.writeFile(Path.join(repoFolder, "LICENSE"), "Draft"), 548 [], 549 { cwd: repoFolder }, 550 ); 551 await peer.rad( 552 ["patch", "ready", patchFive, "--undo"], 553 createOptions(repoFolder, 1), 554 ); 555 } 556 557 export async function createMarkdownFixture(peer: RadiclePeer) { 558 await Fs.mkdir(Path.join(tmpDir, "repos", "markdown")); 559 await execa("tar", [ 560 "-xf", 561 Path.join(fixturesDir, "repos", "markdown.tar.bz2"), 562 "-C", 563 Path.join(tmpDir, "repos", "markdown"), 564 ]); 565 const { repoFolder } = await createRepo(peer, { name: "markdown" }); 566 await Fs.cp(Path.join(tmpDir, "repos", "markdown"), repoFolder, { 567 recursive: true, 568 }); 569 570 await peer.git(["add", "."], { cwd: repoFolder }); 571 const commitMessage = `Add Markdown cheat sheet 572 573 Borrowed from [Adam Pritchard][ap]. 574 No modifications were made. 575 576 [ap]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet`; 577 await peer.git(["commit", "-m", commitMessage], { 578 cwd: repoFolder, 579 }); 580 await peer.git(["push", "rad"], { cwd: repoFolder }); 581 await issue.create( 582 peer, 583 "This `title` has **markdown**", 584 'This is a description\n\nWith some multiline text.\n\n```\n23-11-06 10:19 ➜ radicle-jetbrains-plugin git:(main) rad id update --title "Godify jchrist" --description "where jchrist ascends to a god of this project" --delegate did:key:z6MkpaATbhkGbSMysNomYTFVvKG5bnNKYZ2cCamfoHzX9SnL --threshold 1\n\n✓ Identity revision 029837dde8f5c49704e50a19cd709473ac66a456 created\n```', 585 ["bug", "feature-request"], 586 { cwd: repoFolder }, 587 ); 588 } 589 590 export const aliceMainHead = "7babd25a74eb3752ec24672b5edf0e7ecb4daf24"; 591 export const aliceMainCommitMessage = 592 "Verify that crate::DoubleColon::should_work()"; 593 export const aliceMainCommitCount = 8; 594 export const aliceRemote = 595 "did:key:z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK"; 596 export const shortAliceHead = formatCommit(aliceMainHead); 597 export const bobRemote = 598 "did:key:z6Mkg49NtQR2LyYRDCQFK4w1VVHqhypZSSRo7HsyuN7SV7v5"; 599 export const bobHead = "82f570ec909e77c7e1bb764f1429b1e01b1b4a90"; 600 export const bobMainCommitCount = 9; 601 export const shortBobHead = formatCommit(bobHead); 602 export const sourceBrowsingRid = "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"; 603 export const cobRid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe"; 604 export const markdownRid = "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg"; 605 export const sourceBrowsingUrl = `/nodes/127.0.0.1/${sourceBrowsingRid}`; 606 export const cobUrl = `/nodes/127.0.0.1/${cobRid}`; 607 export const markdownUrl = `/nodes/127.0.0.1/${markdownRid}`; 608 export const shortNodeRemote = "z6MktU…1xB22S"; 609 export const defaultHttpdPort = 8081; 610 export const gitOptions = { 611 alice: { 612 GIT_AUTHOR_NAME: "Alice Liddell", 613 GIT_AUTHOR_EMAIL: "alice@radicle.xyz", 614 GIT_AUTHOR_DATE: "1671125284", 615 GIT_COMMITTER_NAME: "Alice Liddell", 616 GIT_COMMITTER_EMAIL: "alice@radicle.xyz", 617 GIT_COMMITTER_DATE: "1671125284", 618 }, 619 bob: { 620 GIT_AUTHOR_NAME: "Bob Belcher", 621 GIT_AUTHOR_EMAIL: "bob@radicle.xyz", 622 GIT_AUTHOR_DATE: "1671125284", 623 GIT_COMMITTER_NAME: "Bob Belcher", 624 GIT_COMMITTER_EMAIL: "bob@radicle.xyz", 625 GIT_COMMITTER_DATE: "1671627600", 626 }, 627 628 eve: { 629 GIT_AUTHOR_NAME: "Eve Johnson", 630 GIT_AUTHOR_EMAIL: "eve@radicle.xyz", 631 GIT_AUTHOR_DATE: "1671125284", 632 GIT_COMMITTER_NAME: "Eve Johnson", 633 GIT_COMMITTER_EMAIL: "eve@radicle.xyz", 634 GIT_COMMITTER_DATE: "1671627600", 635 }, 636 }; 637 export const defaultConfig: Config = { 638 publicExplorer: "https://app.radicle.xyz/nodes/$host/$rid$path", 639 preferredSeeds: [], 640 web: { 641 pinned: { 642 repositories: ["rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"], 643 }, 644 }, 645 cli: { 646 hints: true, 647 }, 648 node: { 649 alias: "alice", 650 listen: [], 651 peers: { 652 type: "dynamic", 653 }, 654 connect: [], 655 externalAddresses: [], 656 network: "main", 657 log: "INFO", 658 relay: "auto", 659 limits: { 660 routingMaxSize: 1000, 661 routingMaxAge: 604800, 662 gossipMaxAge: 1209600, 663 fetchConcurrency: 1, 664 maxOpenFiles: 4096, 665 rate: { 666 inbound: { 667 fillRate: 5.0, 668 capacity: 1024, 669 }, 670 outbound: { 671 fillRate: 10.0, 672 capacity: 2048, 673 }, 674 }, 675 connection: { 676 inbound: 128, 677 outbound: 16, 678 }, 679 }, 680 workers: 8, 681 seedingPolicy: { 682 default: "block", 683 }, 684 }, 685 };