mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-12 09:24:24 +00:00
169 lines
5.4 KiB
JavaScript
169 lines
5.4 KiB
JavaScript
"use strict";
|
|
const assert = require("node:assert/strict");
|
|
const test = require("node:test");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const os = require("node:os");
|
|
const { PluginDatabase } = require("./database.cjs");
|
|
const { PluginThemeService } = require("./themeService.cjs");
|
|
function fixture(t) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-theme-test-"));
|
|
const database = new PluginDatabase(path.join(root, "plugins.sqlite"));
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(
|
|
__dirname,
|
|
"../../examples/plugins/forest-theme-pack/netcatty.plugin.json",
|
|
),
|
|
"utf8",
|
|
),
|
|
);
|
|
const install = (version = "0.1.0", digest = "a".repeat(64)) =>
|
|
database.installVersion(
|
|
{
|
|
pluginId: manifest.id,
|
|
version,
|
|
manifest: { ...manifest, version },
|
|
archiveSha256: digest,
|
|
packageRelativePath: "theme",
|
|
},
|
|
{ enable: true },
|
|
);
|
|
install();
|
|
for (const mode of ["light", "dark"])
|
|
fs.copyFileSync(
|
|
path.join(
|
|
__dirname,
|
|
"../../examples/plugins/forest-theme-pack",
|
|
mode + ".json",
|
|
),
|
|
path.join(root, mode + ".json"),
|
|
);
|
|
let change;
|
|
const packageStore = {
|
|
async preparePackageRoot() {
|
|
return root;
|
|
},
|
|
};
|
|
const service = new PluginThemeService({
|
|
database,
|
|
packageStore,
|
|
contributionService: {
|
|
onDidChange(listener) {
|
|
change = listener;
|
|
return () => {};
|
|
},
|
|
},
|
|
});
|
|
t.after(() => {
|
|
service.shutdown();
|
|
database.close();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
return {
|
|
root,
|
|
database,
|
|
service,
|
|
packageStore,
|
|
install,
|
|
change: () => change({ reason: "plugin-disabled" }),
|
|
};
|
|
}
|
|
test("data-only themes enumerate without runtime, apply with CAS and fixed audit, retain missing preference after disable", async (t) => {
|
|
const f = fixture(t),
|
|
catalog = await f.service.catalog();
|
|
assert.equal(catalog.entries.length, 2);
|
|
assert.ok(catalog.entries.every((e) => e.dataOnly));
|
|
assert.equal(catalog.selection.revision, 0);
|
|
const ref = catalog.entries[0].ref;
|
|
const applied = await f.service.apply({
|
|
expectedRevision: 0,
|
|
mode: "light",
|
|
choice: { ref },
|
|
});
|
|
assert.equal(applied.selection.light.ref.digest, "a".repeat(64));
|
|
await assert.rejects(
|
|
f.service.apply({ expectedRevision: 0, mode: "light", choice: { ref } }),
|
|
/changed/,
|
|
);
|
|
assert.deepEqual(f.database.listSecurityAudit(ref.pluginId)[0].details, {
|
|
revision: 1,
|
|
action: "apply",
|
|
});
|
|
f.database.setEnabled(ref.pluginId, false);
|
|
f.change();
|
|
const disabled = await f.service.catalog();
|
|
assert.equal(disabled.entries.length, 0);
|
|
assert.deepEqual(disabled.selection.light, { ref });
|
|
await assert.rejects(
|
|
f.service.apply({ expectedRevision: 1, mode: "light", choice: { ref } }),
|
|
/no longer available/,
|
|
);
|
|
const restored = await f.service.apply({
|
|
expectedRevision: 1,
|
|
mode: "light",
|
|
choice: null,
|
|
});
|
|
assert.equal(restored.selection.light, null);
|
|
});
|
|
test("palette validation rejects unsupported values, low contrast, mode mismatch and bounded oversize", async (t) => {
|
|
const f = fixture(t);
|
|
for (const palette of [
|
|
{ formatVersion: 1, base: "light", colors: { accent: "var(--x)" } },
|
|
{ formatVersion: 1, base: "light", colors: { foreground: "#ffffff" } },
|
|
{ formatVersion: 1, base: "light", colors: { muted: "#000000", mutedForeground: "#ffffff" } },
|
|
{ formatVersion: 1, base: "dark", colors: { accent: "#86efac" } },
|
|
" ".repeat(65537),
|
|
]) {
|
|
fs.writeFileSync(
|
|
path.join(f.root, "light.json"),
|
|
typeof palette === "string" ? palette : JSON.stringify(palette),
|
|
);
|
|
f.change();
|
|
const catalog = await f.service.catalog();
|
|
assert.equal(catalog.entries.length, 1);
|
|
assert.equal(catalog.diagnostics[0].code, "invalid");
|
|
}
|
|
});
|
|
test("replacement during an awaited package read cannot publish or apply the old identity", async (t) => {
|
|
const f = fixture(t);
|
|
const before = await f.service.catalog(),
|
|
ref = before.entries[0].ref;
|
|
f.change();
|
|
let once = true;
|
|
f.packageStore.preparePackageRoot = async () => {
|
|
if (once) {
|
|
once = false;
|
|
f.install("0.1.1", "b".repeat(64));
|
|
f.change();
|
|
}
|
|
return f.root;
|
|
};
|
|
const catalog = await f.service.catalog();
|
|
assert.ok(catalog.entries.every((e) => e.ref.version === "0.1.1"));
|
|
await assert.rejects(
|
|
f.service.apply({ expectedRevision: 0, mode: "light", choice: { ref } }),
|
|
/no longer available/,
|
|
);
|
|
});
|
|
test("preference and audit roll back together on audit failure and survive schema-3 migration", async (t) => {
|
|
const f = fixture(t),
|
|
ref = (await f.service.catalog()).entries[0].ref;
|
|
f.database.db.exec(
|
|
"CREATE TRIGGER reject_theme_audit BEFORE INSERT ON plugin_security_audit BEGIN SELECT RAISE(ABORT,'synthetic audit failure'); END",
|
|
);
|
|
await assert.rejects(
|
|
f.service.apply({ expectedRevision: 0, mode: "light", choice: { ref } }),
|
|
/synthetic audit failure/,
|
|
);
|
|
assert.equal(f.database.getHostThemeSelection().revision, 0);
|
|
f.database.db.exec("DROP TRIGGER reject_theme_audit");
|
|
const file = path.join(f.root, "v3.sqlite");
|
|
const old = new PluginDatabase(file);
|
|
old.db.exec("DROP TABLE plugin_host_preferences; PRAGMA user_version=3");
|
|
old.close();
|
|
const migrated = new PluginDatabase(file);
|
|
assert.equal(migrated.getHostThemeSelection().revision, 0);
|
|
migrated.close();
|
|
});
|