Files
kandev/apps/web/components/settings/plugins/plugin-repo-link.test.tsx
T
João Salavisa 91bdee3d3a feat(plugins): show source repo link on installed plugin list and settings (#1821)
* feat(plugins): show source repo link on installed plugin list and settings

Installed plugins carried no repository URL — a "Repo" link existed only
on marketplace catalog cards, whose repo_url is registry-derived from
plugins.yaml and never reaches an installed record. Let the manifest
declare an optional repo_url so sideloaded and directly-installed plugins
surface the same link.

- manifest: add optional RepoURL (repo_url); validate http(s)-only at
  registration so a javascript: scheme can't reach the operator's session.
- Record embeds the manifest, so repo_url flows through JSON to the
  frontend PluginRecord with no extra plumbing.
- frontend: extract the shared, guarded PluginRepoLink (+ isHttpUrl) from
  the marketplace row and render it on the installed plugin row and the
  plugin detail header.
- fixture + docs: exercise repo_url in the e2e fixture manifest and
  document the field in the public manifest reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins): harden repo_url handling per review

- PluginRepoLink accepts string|undefined|null and isHttpUrl null-checks
  before trim, so it can't throw on an absent repo_url (CodeRabbit); drop
  the now-redundant truthiness guards at the call sites.
- validateRepoURL trims m.RepoURL in place so the stored/serialised value
  matches what was validated (Greptile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:03:52 +01:00

39 lines
1.6 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { PluginRepoLink, isHttpUrl } from "./plugin-repo-link";
afterEach(() => cleanup());
describe("isHttpUrl", () => {
it("accepts http and https, rejects other schemes and blanks", () => {
expect(isHttpUrl("https://github.com/kdlbs/kandev-plugin-acme")).toBe(true);
expect(isHttpUrl("http://example.com")).toBe(true);
expect(isHttpUrl(" https://example.com ")).toBe(true);
expect(isHttpUrl("javascript:alert(1)")).toBe(false);
expect(isHttpUrl("ftp://example.com")).toBe(false);
expect(isHttpUrl("")).toBe(false);
expect(isHttpUrl(undefined)).toBe(false);
expect(isHttpUrl(null)).toBe(false);
});
});
describe("PluginRepoLink", () => {
it("renders an external link for an http(s) URL", () => {
render(<PluginRepoLink url="https://github.com/kdlbs/kandev-plugin-acme" />);
const link = screen.getByTestId("plugin-repo-link");
expect(link.getAttribute("href")).toBe("https://github.com/kdlbs/kandev-plugin-acme");
expect(link.getAttribute("target")).toBe("_blank");
expect(link.getAttribute("rel")).toBe("noreferrer");
});
it("renders nothing for a javascript: (or other non-http) scheme", () => {
render(<PluginRepoLink url="javascript:alert(document.cookie)" />);
expect(screen.queryByTestId("plugin-repo-link")).toBeNull();
});
it("renders nothing for an empty URL", () => {
render(<PluginRepoLink url="" />);
expect(screen.queryByTestId("plugin-repo-link")).toBeNull();
});
});