Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
277 lines
7.3 KiB
TypeScript
277 lines
7.3 KiB
TypeScript
/**
|
|
* PluginStateRepository Tests
|
|
*
|
|
* Tests the database-backed plugin state storage for:
|
|
* - CRUD operations (get, getAll, upsert, delete)
|
|
* - Enable/disable convenience methods
|
|
* - Timestamp tracking
|
|
*/
|
|
|
|
import Database from "better-sqlite3";
|
|
import { Kysely, SqliteDialect } from "kysely";
|
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
|
import { runMigrations } from "../../../src/database/migrations/runner.js";
|
|
import type { Database as DbSchema } from "../../../src/database/types.js";
|
|
import { PluginStateRepository } from "../../../src/plugins/state.js";
|
|
|
|
describe("PluginStateRepository", () => {
|
|
let db: Kysely<DbSchema>;
|
|
let sqliteDb: Database.Database;
|
|
let repo: PluginStateRepository;
|
|
|
|
beforeEach(async () => {
|
|
// Create in-memory SQLite database
|
|
sqliteDb = new Database(":memory:");
|
|
|
|
db = new Kysely<DbSchema>({
|
|
dialect: new SqliteDialect({
|
|
database: sqliteDb,
|
|
}),
|
|
});
|
|
|
|
// Run migrations to create tables
|
|
await runMigrations(db);
|
|
|
|
repo = new PluginStateRepository(db);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await db.destroy();
|
|
sqliteDb.close();
|
|
});
|
|
|
|
describe("get", () => {
|
|
it("returns null for non-existent plugin", async () => {
|
|
const state = await repo.get("non-existent");
|
|
expect(state).toBeNull();
|
|
});
|
|
|
|
it("returns state for existing plugin", async () => {
|
|
// Insert directly
|
|
await db
|
|
.insertInto("_plugin_state")
|
|
.values({
|
|
plugin_id: "test-plugin",
|
|
status: "active",
|
|
version: "1.0.0",
|
|
installed_at: new Date().toISOString(),
|
|
activated_at: new Date().toISOString(),
|
|
deactivated_at: null,
|
|
data: null,
|
|
})
|
|
.execute();
|
|
|
|
const state = await repo.get("test-plugin");
|
|
|
|
expect(state).not.toBeNull();
|
|
expect(state!.pluginId).toBe("test-plugin");
|
|
expect(state!.status).toBe("active");
|
|
expect(state!.version).toBe("1.0.0");
|
|
});
|
|
|
|
it("parses dates correctly", async () => {
|
|
const now = new Date();
|
|
await db
|
|
.insertInto("_plugin_state")
|
|
.values({
|
|
plugin_id: "test-plugin",
|
|
status: "inactive",
|
|
version: "2.0.0",
|
|
installed_at: now.toISOString(),
|
|
activated_at: now.toISOString(),
|
|
deactivated_at: now.toISOString(),
|
|
data: null,
|
|
})
|
|
.execute();
|
|
|
|
const state = await repo.get("test-plugin");
|
|
|
|
expect(state!.installedAt).toBeInstanceOf(Date);
|
|
expect(state!.activatedAt).toBeInstanceOf(Date);
|
|
expect(state!.deactivatedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("handles null dates", async () => {
|
|
await db
|
|
.insertInto("_plugin_state")
|
|
.values({
|
|
plugin_id: "test-plugin",
|
|
status: "inactive",
|
|
version: "1.0.0",
|
|
installed_at: new Date().toISOString(),
|
|
activated_at: null,
|
|
deactivated_at: null,
|
|
data: null,
|
|
})
|
|
.execute();
|
|
|
|
const state = await repo.get("test-plugin");
|
|
|
|
expect(state!.activatedAt).toBeNull();
|
|
expect(state!.deactivatedAt).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getAll", () => {
|
|
it("returns empty array when no plugins", async () => {
|
|
const states = await repo.getAll();
|
|
expect(states).toEqual([]);
|
|
});
|
|
|
|
it("returns all plugin states", async () => {
|
|
await db
|
|
.insertInto("_plugin_state")
|
|
.values([
|
|
{
|
|
plugin_id: "plugin-a",
|
|
status: "active",
|
|
version: "1.0.0",
|
|
installed_at: new Date().toISOString(),
|
|
activated_at: new Date().toISOString(),
|
|
deactivated_at: null,
|
|
data: null,
|
|
},
|
|
{
|
|
plugin_id: "plugin-b",
|
|
status: "inactive",
|
|
version: "2.0.0",
|
|
installed_at: new Date().toISOString(),
|
|
activated_at: null,
|
|
deactivated_at: null,
|
|
data: null,
|
|
},
|
|
])
|
|
.execute();
|
|
|
|
const states = await repo.getAll();
|
|
|
|
expect(states).toHaveLength(2);
|
|
expect(states.map((s) => s.pluginId).toSorted()).toEqual(["plugin-a", "plugin-b"]);
|
|
});
|
|
});
|
|
|
|
describe("upsert", () => {
|
|
it("creates new state when plugin does not exist", async () => {
|
|
const state = await repo.upsert("new-plugin", "1.0.0", "active");
|
|
|
|
expect(state.pluginId).toBe("new-plugin");
|
|
expect(state.version).toBe("1.0.0");
|
|
expect(state.status).toBe("active");
|
|
expect(state.installedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("updates existing state", async () => {
|
|
// Create initial state
|
|
await repo.upsert("test-plugin", "1.0.0", "active");
|
|
|
|
// Update it
|
|
const state = await repo.upsert("test-plugin", "1.1.0", "inactive");
|
|
|
|
expect(state.pluginId).toBe("test-plugin");
|
|
expect(state.version).toBe("1.1.0");
|
|
expect(state.status).toBe("inactive");
|
|
});
|
|
|
|
it("sets activated_at when activating", async () => {
|
|
// Create as inactive
|
|
await repo.upsert("test-plugin", "1.0.0", "inactive");
|
|
|
|
// Activate
|
|
const state = await repo.upsert("test-plugin", "1.0.0", "active");
|
|
|
|
expect(state.activatedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("sets deactivated_at when deactivating", async () => {
|
|
// Create as active
|
|
await repo.upsert("test-plugin", "1.0.0", "active");
|
|
|
|
// Deactivate
|
|
const state = await repo.upsert("test-plugin", "1.0.0", "inactive");
|
|
|
|
expect(state.deactivatedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("does not change activated_at if already active", async () => {
|
|
// Create as active
|
|
const initial = await repo.upsert("test-plugin", "1.0.0", "active");
|
|
const initialActivatedAt = initial.activatedAt!.getTime();
|
|
|
|
// Wait a bit then update version (still active)
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
const updated = await repo.upsert("test-plugin", "1.1.0", "active");
|
|
|
|
// activated_at should be the same
|
|
expect(updated.activatedAt!.getTime()).toBe(initialActivatedAt);
|
|
});
|
|
});
|
|
|
|
describe("enable", () => {
|
|
it("creates active state for new plugin", async () => {
|
|
const state = await repo.enable("new-plugin", "1.0.0");
|
|
|
|
expect(state.status).toBe("active");
|
|
expect(state.activatedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("activates inactive plugin", async () => {
|
|
await repo.upsert("test-plugin", "1.0.0", "inactive");
|
|
|
|
const state = await repo.enable("test-plugin", "1.0.0");
|
|
|
|
expect(state.status).toBe("active");
|
|
});
|
|
});
|
|
|
|
describe("disable", () => {
|
|
it("creates inactive state for new plugin", async () => {
|
|
const state = await repo.disable("new-plugin", "1.0.0");
|
|
|
|
expect(state.status).toBe("inactive");
|
|
expect(state.activatedAt).toBeNull();
|
|
});
|
|
|
|
it("deactivates active plugin", async () => {
|
|
await repo.upsert("test-plugin", "1.0.0", "active");
|
|
|
|
const state = await repo.disable("test-plugin", "1.0.0");
|
|
|
|
expect(state.status).toBe("inactive");
|
|
expect(state.deactivatedAt).toBeInstanceOf(Date);
|
|
});
|
|
});
|
|
|
|
describe("delete", () => {
|
|
it("returns false for non-existent plugin", async () => {
|
|
const deleted = await repo.delete("non-existent");
|
|
expect(deleted).toBe(false);
|
|
});
|
|
|
|
it("deletes existing plugin and returns true", async () => {
|
|
await repo.upsert("test-plugin", "1.0.0", "active");
|
|
|
|
const deleted = await repo.delete("test-plugin");
|
|
|
|
expect(deleted).toBe(true);
|
|
|
|
// Verify it's gone
|
|
const state = await repo.get("test-plugin");
|
|
expect(state).toBeNull();
|
|
});
|
|
|
|
it("only deletes specified plugin", async () => {
|
|
await repo.upsert("plugin-a", "1.0.0", "active");
|
|
await repo.upsert("plugin-b", "1.0.0", "active");
|
|
|
|
await repo.delete("plugin-a");
|
|
|
|
const stateA = await repo.get("plugin-a");
|
|
const stateB = await repo.get("plugin-b");
|
|
|
|
expect(stateA).toBeNull();
|
|
expect(stateB).not.toBeNull();
|
|
});
|
|
});
|
|
});
|