Bun-based command-line client covering the Linkwarden API: links, collections, tags, highlights, RSS, tokens, settings, import and export. Includes unit tests, an in-memory mock server suite, and an opt-in e2e suite for a real server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
456 lines
21 KiB
TypeScript
456 lines
21 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
import { createEnv, type Env } from "./helpers.ts";
|
|
import { createMockServer } from "./mock-server.ts";
|
|
|
|
const mock = createMockServer();
|
|
let env: Env;
|
|
const auth = () => ({ LINKWARDEN_URL: mock.url, LINKWARDEN_TOKEN: mock.token });
|
|
|
|
beforeAll(async () => {
|
|
env = await createEnv();
|
|
});
|
|
afterAll(async () => {
|
|
mock.stop();
|
|
await env.cleanup();
|
|
});
|
|
beforeEach(() => {
|
|
mock.requests.length = 0;
|
|
});
|
|
|
|
describe("help and usage", () => {
|
|
test("prints top-level help", async () => {
|
|
const r = await env.run([]);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain("Commands");
|
|
expect(r.stdout).toContain("collections");
|
|
});
|
|
|
|
test("prints group help and command help", async () => {
|
|
expect((await env.run(["links"])).stdout).toContain("clear-archives");
|
|
const r = await env.run(["links", "add", "--help"]);
|
|
expect(r.stdout).toContain("--collection");
|
|
expect(r.stdout).toContain("Examples");
|
|
expect((await env.run(["help", "tags", "merge"])).stdout).toContain("--into");
|
|
});
|
|
|
|
test("prints the version", async () => {
|
|
expect((await env.run(["--version"])).stdout.trim()).toBe("0.0.0-test");
|
|
});
|
|
|
|
test("rejects unknown commands and options with exit code 2", async () => {
|
|
expect((await env.run(["nope"])).code).toBe(2);
|
|
expect((await env.run(["links", "nope"])).code).toBe(2);
|
|
const r = await env.run(["links", "list", "--bogus"]);
|
|
expect(r.code).toBe(2);
|
|
expect(r.stderr).toContain("bogus");
|
|
});
|
|
|
|
test("exits with code 3 when not logged in", async () => {
|
|
const r = await env.run(["links", "list"]);
|
|
expect(r.code).toBe(3);
|
|
expect(r.stderr).toContain("Not logged in");
|
|
});
|
|
|
|
test("accepts global flags before the command", async () => {
|
|
const r = await env.run(["--json", "--url", mock.url, "--token", mock.token, "links", "list"]);
|
|
expect(r.code).toBe(0);
|
|
expect(r.json().links.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("auth", () => {
|
|
test("login with password stores a 0600 config and uses the session token", async () => {
|
|
const r = await env.run(["login", "--url", mock.url, "-u", "alice", "--password", "password123"]);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain("alice");
|
|
const saved = await Bun.file(env.configPath).json();
|
|
expect(saved.profiles.default).toMatchObject({ url: mock.url, token: mock.token, username: "alice" });
|
|
const { statSync } = await import("node:fs");
|
|
expect(statSync(env.configPath).mode & 0o777).toBe(0o600);
|
|
expect(mock.find("POST", "/session")[0]!.body.sessionName).toStartWith("linkwarden-cli@");
|
|
// The saved profile authenticates later commands.
|
|
const who = await env.run(["whoami", "--json"]);
|
|
expect(who.json().username).toBe("alice");
|
|
});
|
|
|
|
test("login with a bad password fails without saving", async () => {
|
|
const other = await createEnv();
|
|
const r = await other.run(["login", "--url", mock.url, "-u", "alice", "--password", "wrongpass1"]);
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain("Invalid credentials");
|
|
expect(await Bun.file(other.configPath).exists()).toBe(false);
|
|
await other.cleanup();
|
|
});
|
|
|
|
test("login with an access token verifies it", async () => {
|
|
const other = await createEnv();
|
|
expect((await other.run(["login", "--url", mock.url, "--token", "bad"])).code).toBe(3);
|
|
const ok = await other.run(["login", "--url", `${mock.url}/api/v1/`, "--token", mock.token, "--profile", "work"]);
|
|
expect(ok.code).toBe(0);
|
|
const saved = await Bun.file(other.configPath).json();
|
|
expect(saved.profiles.work.url).toBe(mock.url);
|
|
expect(saved.current).toBe("work");
|
|
const list = await other.run(["profile", "list", "--json"]);
|
|
expect(list.json()[0].name).toBe("work");
|
|
expect((await other.run(["logout"])).code).toBe(0);
|
|
expect((await other.run(["whoami"])).code).toBe(3);
|
|
await other.cleanup();
|
|
});
|
|
|
|
test("environment variables override the profile", async () => {
|
|
const r = await env.run(["whoami", "--json"], { LINKWARDEN_URL: mock.url, LINKWARDEN_TOKEN: "wrong" });
|
|
expect(r.code).toBe(3);
|
|
});
|
|
|
|
test("an expired token suggests logging in again", async () => {
|
|
const r = await env.run(["links", "list"], { LINKWARDEN_URL: mock.url, LINKWARDEN_TOKEN: "stale" });
|
|
expect(r.code).toBe(3);
|
|
expect(r.stderr).toContain("linkwarden login");
|
|
});
|
|
|
|
test("a 401 permission error is not reported as an authentication failure", async () => {
|
|
const r = await env.run(["api", "GET", "/collections/99"], auth());
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain("Collection is not accessible");
|
|
expect(r.stderr).not.toContain("linkwarden login");
|
|
});
|
|
|
|
test("info works without authentication", async () => {
|
|
const r = await env.run(["info", "--json"], { LINKWARDEN_URL: mock.url, LINKWARDEN_TOKEN: "x" });
|
|
expect(r.json().PAGINATION_TAKE_COUNT).toBe(50);
|
|
});
|
|
});
|
|
|
|
describe("links", () => {
|
|
test("list paginates with --all and sends the sort", async () => {
|
|
const r = await env.run(["links", "list", "--all", "--sort", "name-asc", "--json"], auth());
|
|
expect(r.json().links.map((l: any) => l.id)).toEqual([1, 2, 3]);
|
|
const searches = mock.find("GET", "/search");
|
|
expect(searches).toHaveLength(2);
|
|
expect(searches[0]!.query.sort).toBe("2");
|
|
expect(searches[1]!.query.cursor).toBe("2");
|
|
});
|
|
|
|
test("list honors --limit and reports the next cursor", async () => {
|
|
const r = await env.run(["links", "list", "-n", "1", "--json"], auth());
|
|
expect(r.json().links).toHaveLength(1);
|
|
});
|
|
|
|
test("list resolves collection names and renders a table", async () => {
|
|
const r = await env.run(["ls", "-c", "dev", "--no-color"], auth());
|
|
expect(r.code).toBe(0);
|
|
expect(mock.find("GET", "/search")[0]!.query.collectionId).toBe("2");
|
|
expect(r.stdout).toContain("Bun docs");
|
|
expect(r.stdout).not.toContain("Example");
|
|
});
|
|
|
|
test("list --tag filters client-side because the server ORs the tag condition", async () => {
|
|
const r = await env.run(["links", "list", "-t", "todo", "--all", "--json"], auth());
|
|
expect(r.json().links.map((l: any) => l.id)).toEqual([2]);
|
|
expect(mock.find("GET", "/search")[0]!.query.tagId).toBe("11");
|
|
});
|
|
|
|
test("list --pinned filters client-side", async () => {
|
|
const r = await env.run(["links", "list", "--pinned", "--all", "-q"], auth());
|
|
expect(r.stdout.trim()).toBe("1");
|
|
});
|
|
|
|
test("search sends the query, quiet prints ids", async () => {
|
|
const r = await env.run(["search", "bun", "docs", "-q"], auth());
|
|
expect(mock.find("GET", "/search")[0]!.query.searchQueryString).toBe("bun docs");
|
|
expect(r.stdout.trim()).toBe("2");
|
|
expect((await env.run(["search"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("unknown collection is reported", async () => {
|
|
const r = await env.run(["ls", "-c", "nope"], auth());
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain('Collection "nope" not found');
|
|
});
|
|
|
|
test("get shows details and archive state", async () => {
|
|
const r = await env.run(["links", "get", "2"], auth());
|
|
expect(r.stdout).toContain("Bun docs");
|
|
expect(r.stdout).toContain("PDF");
|
|
expect((await env.run(["links", "get", "999"], auth())).code).toBe(1);
|
|
expect((await env.run(["links", "get", "abc"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("add sends tags, resolves the collection and prefixes https", async () => {
|
|
const r = await env.run(["add", "example.org/page", "-c", "Dev", "-t", "a,b", "-t", "c", "-d", "note"], auth());
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain("Added");
|
|
const body = mock.find("POST", "/links")[0]!.body;
|
|
expect(body.url).toBe("https://example.org/page");
|
|
expect(body.collection).toEqual({ id: 2 });
|
|
expect(body.tags.map((t: any) => t.name)).toEqual(["a", "b", "c"]);
|
|
expect(body.description).toBe("note");
|
|
});
|
|
|
|
test("add refuses unknown collections unless asked to create them", async () => {
|
|
expect((await env.run(["add", "https://x.test", "-c", "New"], auth())).code).toBe(1);
|
|
expect(mock.find("POST", "/links")).toHaveLength(0);
|
|
await env.run(["add", "https://x.test", "-c", "New", "--create-collection"], auth());
|
|
expect(mock.find("POST", "/links")[0]!.body.collection).toEqual({ name: "New" });
|
|
});
|
|
|
|
test("add continues after a failure and exits 1", async () => {
|
|
const r = await env.run(["add", "https://dup.test", "https://ok.test"], auth());
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain("Link already exists");
|
|
expect(mock.find("POST", "/links")).toHaveLength(2);
|
|
});
|
|
|
|
test("add --pin pins the new link", async () => {
|
|
await env.run(["add", "https://pin.test", "--pin"], auth());
|
|
const put = mock.requests.find((r) => r.method === "PUT" && r.path.startsWith("/links/"))!;
|
|
expect(put.body.pinnedBy).toEqual([{ id: 1 }]);
|
|
});
|
|
|
|
test("add rejects text that is not a URL but still adds the others", async () => {
|
|
const r = await env.run(["add", "notaurl", "https://fine.test"], auth());
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain('"notaurl" is not a valid URL');
|
|
expect(mock.find("POST", "/links")).toHaveLength(1);
|
|
});
|
|
|
|
test("add rejects --name with several URLs", async () => {
|
|
expect((await env.run(["add", "https://a.test", "https://b.test", "-n", "x"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("add --file uploads a PDF in two steps", async () => {
|
|
const path = `${env.dir}/paper.pdf`;
|
|
await Bun.write(path, "%PDF-1.4");
|
|
const r = await env.run(["add", "-f", path, "-c", "Dev"], auth());
|
|
expect(r.code).toBe(0);
|
|
const create = mock.find("POST", "/links")[0]!.body;
|
|
expect(create).toMatchObject({ type: "pdf", name: "paper" });
|
|
const upload = mock.requests.find((x) => x.path.startsWith("/archives/"))!;
|
|
expect(upload.query.format).toBe("2");
|
|
expect((upload.body as FormData).get("file")).toBeInstanceOf(File);
|
|
expect((await env.run(["add", "-f", `${env.dir}/x.txt`], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("update merges the current link with the changes and replaces tags", async () => {
|
|
const r = await env.run(["links", "update", "2", "-n", "Renamed", "-c", "Rust", "--add-tag", "new", "--remove-tag", "TODO"], auth());
|
|
expect(r.code).toBe(0);
|
|
const body = mock.find("PUT", "/links/2")[0]!.body;
|
|
expect(body).toMatchObject({ id: 2, name: "Renamed", url: "https://bun.sh/docs", collection: { id: 3, ownerId: 1 } });
|
|
expect(body.tags.map((t: any) => t.name)).toEqual(["article", "new"]);
|
|
expect(body.pinnedBy).toBeUndefined();
|
|
});
|
|
|
|
test("update requires a change", async () => {
|
|
expect((await env.run(["links", "update", "2"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("update --set-tags with an empty value clears tags", async () => {
|
|
await env.run(["links", "update", "2", "--set-tags", ""], auth());
|
|
expect(mock.find("PUT", "/links/2")[0]!.body.tags).toEqual([]);
|
|
});
|
|
|
|
test("pin and unpin send the pinnedBy marker", async () => {
|
|
await env.run(["links", "pin", "2"], auth());
|
|
expect(mock.find("PUT", "/links/2")[0]!.body.pinnedBy).toEqual([{ id: 1 }]);
|
|
await env.run(["links", "unpin", "2"], auth());
|
|
expect(mock.find("PUT", "/links/2")[1]!.body.pinnedBy).toEqual([{}]);
|
|
});
|
|
|
|
test("delete needs --yes outside a terminal", async () => {
|
|
expect((await env.run(["links", "delete", "2"], auth())).code).toBe(2);
|
|
const r = await env.run(["rm", "x"], auth());
|
|
expect(r.code).toBe(2);
|
|
const ok = await env.run(["links", "delete", "2", "3", "-y"], auth());
|
|
expect(ok.code).toBe(0);
|
|
expect(mock.find("DELETE", "/links")[0]!.body).toEqual({ linkIds: [2, 3] });
|
|
});
|
|
|
|
test("refresh and clear-archives call the archive endpoints", async () => {
|
|
await env.run(["links", "refresh", "1", "2"], auth());
|
|
expect(mock.find("PUT", "/links/1/archive")).toHaveLength(1);
|
|
await env.run(["links", "clear-archives", "1", "-y"], auth());
|
|
expect(mock.find("DELETE", "/links/archive")[0]!.body).toEqual({ linkIds: [1] });
|
|
});
|
|
|
|
test("download picks the first available format and writes the file", async () => {
|
|
const out = `${env.dir}/out.pdf`;
|
|
const r = await env.run(["links", "download", "2", "-o", out], auth());
|
|
expect(r.code).toBe(0);
|
|
expect(mock.find("GET", "/archives/2")[0]!.query.format).toBe("2");
|
|
expect(await Bun.file(out).text()).toBe("%PDF");
|
|
const none = await env.run(["links", "download", "1"], auth());
|
|
expect(none.stderr).toContain("no stored copies");
|
|
});
|
|
|
|
test("open --print outputs the URL", async () => {
|
|
const r = await env.run(["open", "1", "--print"], auth());
|
|
expect(r.stdout.trim()).toBe("https://example.com/");
|
|
const web = await env.run(["open", "1", "--print", "--web"], auth());
|
|
expect(web.stdout.trim()).toBe(`${mock.url}/links/1`);
|
|
});
|
|
});
|
|
|
|
describe("collections", () => {
|
|
test("list renders a tree", async () => {
|
|
const r = await env.run(["collections", "list"], auth());
|
|
const lines = r.stdout.split("\n");
|
|
expect(lines.findIndex((l) => l.includes("Dev"))).toBeLessThan(lines.findIndex((l) => l.includes("Rust")));
|
|
expect(r.stdout).toContain(" Rust");
|
|
});
|
|
|
|
test("get resolves a path", async () => {
|
|
const r = await env.run(["collections", "get", "Dev/Rust", "--json"], auth());
|
|
expect(r.json().id).toBe(3);
|
|
});
|
|
|
|
test("create with a parent", async () => {
|
|
await env.run(["collections", "create", "New one", "-p", "Dev", "-d", "hi"], auth());
|
|
expect(mock.find("POST", "/collections")[0]!.body).toMatchObject({ name: "New one", parentId: 2, description: "hi" });
|
|
});
|
|
|
|
test("update keeps members and supports --root and --public", async () => {
|
|
await env.run(["collections", "update", "Rust", "--root", "--public", "-n", "Rusty"], auth());
|
|
const body = mock.find("PUT", "/collections/3")[0]!.body;
|
|
expect(body).toMatchObject({ id: 3, name: "Rusty", parentId: "root", isPublic: true, members: [] });
|
|
await env.run(["collections", "update", "Dev", "-d", "x"], auth());
|
|
expect(mock.find("PUT", "/collections/2")[0]!.body.members).toEqual([{ userId: 2, canCreate: true, canUpdate: false, canDelete: false }]);
|
|
expect((await env.run(["collections", "update", "Dev", "--public", "--private"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("share adds a member by username and keeps the others", async () => {
|
|
const r = await env.run(["collections", "share", "Rust", "@Bob", "--can-update"], auth());
|
|
expect(r.code).toBe(0);
|
|
expect(mock.find("GET", "/public/users/bob")).toHaveLength(1);
|
|
expect(mock.find("PUT", "/collections/3")[0]!.body.members).toEqual([{ userId: 2, canCreate: false, canUpdate: true, canDelete: false }]);
|
|
await env.run(["collections", "share", "Dev", "bob", "--can-delete"], auth());
|
|
expect(mock.find("PUT", "/collections/2")[0]!.body.members).toEqual([{ userId: 2, canCreate: true, canUpdate: false, canDelete: true }]);
|
|
expect((await env.run(["collections", "share", "Dev", "ghost"], auth())).stderr).toContain('User "ghost" not found');
|
|
expect((await env.run(["collections", "share", "Dev", "alice"], auth())).code).toBe(2);
|
|
});
|
|
|
|
test("unshare removes a member", async () => {
|
|
expect((await env.run(["collections", "unshare", "Dev", "bob"], auth())).code).toBe(0);
|
|
expect(mock.find("PUT", "/collections/2")[0]!.body.members).toEqual([]);
|
|
expect((await env.run(["collections", "unshare", "Dev", "zed"], auth())).code).toBe(1);
|
|
});
|
|
|
|
test("delete requires --yes", async () => {
|
|
expect((await env.run(["collections", "delete", "Dev"], auth())).code).toBe(2);
|
|
expect((await env.run(["collections", "delete", "Dev", "-y"], auth())).code).toBe(0);
|
|
expect(mock.find("DELETE", "/collections/2")).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("tags", () => {
|
|
test("list shows counts", async () => {
|
|
const r = await env.run(["tags", "list"], auth());
|
|
expect(r.stdout).toContain("article");
|
|
expect(r.stdout).toContain("2");
|
|
});
|
|
|
|
test("create, rename, merge and delete", async () => {
|
|
expect((await env.run(["tags", "create", "a,b"], auth())).stdout).toContain("Created");
|
|
expect(mock.find("POST", "/tags")[0]!.body).toEqual({ tags: [{ label: "a" }, { label: "b" }] });
|
|
await env.run(["tags", "rename", "todo", "later"], auth());
|
|
expect(mock.find("PUT", "/tags/11")[0]!.body).toEqual({ name: "later" });
|
|
await env.run(["tags", "merge", "todo", "article", "--into", "all"], auth());
|
|
expect(mock.find("PUT", "/tags/merge")[0]!.body).toEqual({ newTagName: "all", tagIds: [11, 10] });
|
|
await env.run(["tags", "delete", "10", "-y"], auth());
|
|
expect(mock.find("DELETE", "/tags")[0]!.body).toEqual({ tagIds: [10] });
|
|
expect((await env.run(["tags", "merge", "todo"], auth())).code).toBe(2);
|
|
expect((await env.run(["tags", "rename", "ghost", "x"], auth())).code).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("highlights, tokens, rss", () => {
|
|
test("highlights", async () => {
|
|
expect((await env.run(["highlights", "list", "1"], auth())).stdout).toContain("text");
|
|
await env.run(["highlights", "add", "1", "--text", "hello", "--start", "0", "--end", "5"], auth());
|
|
expect(mock.find("POST", "/highlights")[0]!.body).toMatchObject({ linkId: 1, text: "hello", startOffset: 0, endOffset: 5, color: "yellow" });
|
|
expect((await env.run(["highlights", "add", "1", "--text", "x", "--start", "5", "--end", "2"], auth())).code).toBe(2);
|
|
await env.run(["highlights", "delete", "7"], auth());
|
|
expect(mock.find("DELETE", "/highlights/7")).toHaveLength(1);
|
|
});
|
|
|
|
test("tokens", async () => {
|
|
expect((await env.run(["tokens", "list"], auth())).stdout).toContain("cli");
|
|
const created = await env.run(["tokens", "create", "ci", "-e", "never", "-q"], auth());
|
|
expect(created.stdout.trim()).toBe("secret-jwt");
|
|
expect(mock.find("POST", "/tokens")[0]!.body).toEqual({ name: "ci", expires: 4 });
|
|
expect((await env.run(["tokens", "create", "x", "-e", "forever"], auth())).code).toBe(2);
|
|
await env.run(["tokens", "revoke", "2"], auth());
|
|
expect(mock.find("DELETE", "/tokens/2")).toHaveLength(1);
|
|
});
|
|
|
|
test("rss", async () => {
|
|
expect((await env.run(["rss", "list"], auth())).stdout).toContain("Feed");
|
|
await env.run(["rss", "add", "https://blog.test/feed.xml", "-c", "Dev"], auth());
|
|
expect(mock.find("POST", "/rss")[0]!.body).toEqual({ name: "blog.test", url: "https://blog.test/feed.xml", collectionId: 2 });
|
|
await env.run(["rss", "delete", "1"], auth());
|
|
expect(mock.find("DELETE", "/rss/1")).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("settings", () => {
|
|
test("get shows current values", async () => {
|
|
const r = await env.run(["settings", "get"], auth());
|
|
expect(r.stdout).toContain("Archive PDF");
|
|
expect(r.stdout).toContain("on");
|
|
});
|
|
|
|
test("set sends the username with the changes", async () => {
|
|
const r = await env.run(["settings", "set", "--archive-pdf", "false", "--links-route-to", "details", "--name", "Al"], auth());
|
|
expect(r.code).toBe(0);
|
|
expect(mock.find("PUT", "/users/1")[0]!.body).toEqual({ username: "alice", name: "Al", archiveAsPDF: false, linksRouteTo: "DETAILS" });
|
|
expect((await env.run(["settings", "set"], auth())).code).toBe(2);
|
|
expect((await env.run(["settings", "set", "--archive-pdf", "maybe"], auth())).code).toBe(2);
|
|
expect((await env.run(["settings", "set", "--links-route-to", "nowhere"], auth())).code).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("data, dashboard, public, api, completion", () => {
|
|
test("export writes a file", async () => {
|
|
const out = `${env.dir}/backup.json`;
|
|
expect((await env.run(["export", "-o", out], auth())).code).toBe(0);
|
|
expect(await Bun.file(out).json()).toHaveProperty("collections");
|
|
});
|
|
|
|
test("import detects the format and sends the file text", async () => {
|
|
const html = `${env.dir}/b.html`;
|
|
await Bun.write(html, "<DL></DL>");
|
|
await env.run(["import", html], auth());
|
|
expect(mock.find("POST", "/migration")[0]!.body).toEqual({ format: 1, data: "<DL></DL>" });
|
|
const json = `${env.dir}/b.json`;
|
|
await Bun.write(json, '{"collections":[]}');
|
|
await env.run(["import", json], auth());
|
|
expect(mock.find("POST", "/migration")[1]!.body.format).toBe(0);
|
|
await Bun.write(json, '{"x":1}');
|
|
expect((await env.run(["import", json], auth())).code).toBe(2);
|
|
expect((await env.run(["import", json, "-f", "wallabag"], auth())).code).toBe(0);
|
|
});
|
|
|
|
test("dashboard shows pinned and recent", async () => {
|
|
const r = await env.run(["dashboard", "--json"], auth());
|
|
expect(r.json().pinned.map((l: any) => l.id)).toEqual([1]);
|
|
});
|
|
|
|
test("public collections work without a token", async () => {
|
|
const r = await env.run(["public", "links", "1", "--json"], { LINKWARDEN_URL: mock.url });
|
|
expect(r.json().links).toHaveLength(1);
|
|
expect(mock.requests.at(-1)!.auth).toBeNull();
|
|
});
|
|
|
|
test("api passes through method, query and body", async () => {
|
|
const r = await env.run(["api", "get", "/search", "--query", "searchQueryString=bun"], auth());
|
|
expect(JSON.parse(r.stdout).data.links).toHaveLength(1);
|
|
expect((await env.run(["api", "PUT", "/tags/merge", "-d", "{bad"], auth())).code).toBe(2);
|
|
expect((await env.run(["api", "GET", "/nope"], auth())).code).toBe(1);
|
|
});
|
|
|
|
test("completion scripts", async () => {
|
|
expect((await env.run(["completion", "bash"])).stdout).toContain("complete -F _linkwarden");
|
|
expect((await env.run(["completion", "zsh"])).stdout).toContain("#compdef");
|
|
expect((await env.run(["completion", "fish"])).stdout).toContain("complete -c linkwarden");
|
|
expect((await env.run(["completion", "tcsh"])).code).toBe(2);
|
|
});
|
|
});
|