Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions cli/src/__tests__/lib/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { writeFileSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
readManifest,
addToManifest,
removeFromManifest,
} from "../../lib/manifest.js";

describe("manifest", () => {
let tempDir: string;

beforeEach(() => {
tempDir = join(tmpdir(), "neoboard-test-" + Date.now());
mkdirSync(tempDir, { recursive: true });
});

afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});

describe("readManifest", () => {
it("returns empty arrays when file does not exist", () => {
const result = readManifest(join(tempDir, "missing.json"), "plugins");
expect(result).toEqual([]);
});

it("reads existing plugins", () => {
const path = join(tempDir, "plugins.json");
writeFileSync(
path,
JSON.stringify({
plugins: [{ package: "@myorg/heatmap" }],
}),
);
const result = readManifest(path, "plugins");
expect(result).toHaveLength(1);
expect(result[0].package).toBe("@myorg/heatmap");
});

it("reads existing connectors", () => {
const path = join(tempDir, "connectors.json");
writeFileSync(
path,
JSON.stringify({
connectors: [{ package: "@myorg/mongodb" }],
}),
);
const result = readManifest(path, "connectors");
expect(result).toHaveLength(1);
expect(result[0].package).toBe("@myorg/mongodb");
});
});

describe("addToManifest", () => {
it("creates file and adds entry when file does not exist", () => {
const path = join(tempDir, "new.json");
addToManifest(path, "plugins", { package: "@myorg/heatmap" });
const result = readManifest(path, "plugins");
expect(result).toHaveLength(1);
expect(result[0].package).toBe("@myorg/heatmap");
});

it("appends to existing entries", () => {
const path = join(tempDir, "existing.json");
writeFileSync(
path,
JSON.stringify({ plugins: [{ package: "@myorg/a" }] }),
);
addToManifest(path, "plugins", { package: "@myorg/b" });
const result = readManifest(path, "plugins");
expect(result).toHaveLength(2);
});

it("does not duplicate existing package", () => {
const path = join(tempDir, "dup.json");
writeFileSync(
path,
JSON.stringify({ plugins: [{ package: "@myorg/a" }] }),
);
addToManifest(path, "plugins", { package: "@myorg/a" });
const result = readManifest(path, "plugins");
expect(result).toHaveLength(1);
});

it("supports overrides flag", () => {
const path = join(tempDir, "override.json");
addToManifest(path, "plugins", {
package: "@myorg/bar",
overrides: true,
});
const result = readManifest(path, "plugins");
expect(result[0].overrides).toBe(true);
});
});

describe("removeFromManifest", () => {
it("removes an entry by package name", () => {
const path = join(tempDir, "remove.json");
writeFileSync(
path,
JSON.stringify({
plugins: [{ package: "@myorg/a" }, { package: "@myorg/b" }],
}),
);
const removed = removeFromManifest(path, "plugins", "@myorg/a");
expect(removed).toBe(true);
const result = readManifest(path, "plugins");
expect(result).toHaveLength(1);
expect(result[0].package).toBe("@myorg/b");
});

it("returns false when package not found", () => {
const path = join(tempDir, "notfound.json");
writeFileSync(
path,
JSON.stringify({ plugins: [{ package: "@myorg/a" }] }),
);
const removed = removeFromManifest(path, "plugins", "@myorg/missing");
expect(removed).toBe(false);
});

it("returns false when file does not exist", () => {
const removed = removeFromManifest(
join(tempDir, "missing.json"),
"plugins",
"@myorg/a",
);
expect(removed).toBe(false);
});
});
});
136 changes: 136 additions & 0 deletions cli/src/__tests__/lib/plugin-validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { describe, it, expect } from "vitest";
import {
validatePluginExport,
detectPluginType,
} from "../../lib/plugin-validator.js";

describe("validatePluginExport", () => {
it("accepts a valid chart plugin", () => {
const plugin = {
type: "heatmap",
label: "Heatmap",
component: () => null,
transform: (d: unknown) => d,
compatibleWith: ["neo4j", "postgresql"],
};
const result = validatePluginExport(plugin);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});

it("accepts a valid connector plugin", () => {
const plugin = {
type: "mongodb",
label: "MongoDB",
category: "database",
createModule: () => ({}),
};
const result = validatePluginExport(plugin);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});

it("rejects null", () => {
const result = validatePluginExport(null);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain("must be an object");
});

it("rejects missing type", () => {
const result = validatePluginExport({ label: "X", transform: () => {} });
expect(result.valid).toBe(false);
expect(result.errors).toContain('"type" must be a non-empty string');
});

it("rejects missing label", () => {
const result = validatePluginExport({ type: "x", transform: () => {} });
expect(result.valid).toBe(false);
expect(result.errors).toContain('"label" must be a non-empty string');
});

it("rejects empty type", () => {
const result = validatePluginExport({
type: "",
label: "X",
transform: () => {},
});
expect(result.valid).toBe(false);
expect(result.errors).toContain('"type" must be a non-empty string');
});

it("rejects plugin with neither transform nor createModule", () => {
const result = validatePluginExport({ type: "x", label: "X" });
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain("Not a valid NeoBoard plugin");
});

it("rejects chart plugin missing compatibleWith", () => {
const result = validatePluginExport({
type: "x",
label: "X",
transform: () => {},
});
expect(result.valid).toBe(false);
expect(result.errors).toContain(
'"compatibleWith" must be a non-empty array of connector types',
);
});

it("rejects connector plugin missing category", () => {
const result = validatePluginExport({
type: "x",
label: "X",
createModule: () => ({}),
});
expect(result.valid).toBe(false);
expect(result.errors).toContain(
'"category" must be one of: database, graph, api, file',
);
});

it("rejects connector with invalid category", () => {
const result = validatePluginExport({
type: "x",
label: "X",
category: "invalid",
createModule: () => ({}),
});
expect(result.valid).toBe(false);
expect(result.errors).toContain(
'"category" must be one of: database, graph, api, file',
);
});
});

describe("detectPluginType", () => {
it("detects chart plugin", () => {
expect(
detectPluginType({ type: "x", label: "X", transform: () => {} }),
).toBe("chart");
});

it("detects connector plugin", () => {
expect(
detectPluginType({
type: "x",
label: "X",
createModule: () => ({}),
}),
).toBe("connector");
});

it("returns null for ambiguous (both)", () => {
expect(
detectPluginType({
type: "x",
label: "X",
transform: () => {},
createModule: () => ({}),
}),
).toBe(null);
});

it("returns null for neither", () => {
expect(detectPluginType({ type: "x", label: "X" })).toBe(null);
});
});
Loading
Loading