/ tests / support / fixtures.ts
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  };