import type { Kysely } from "kysely"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { PluginStorageRepository } from "../../../src/database/repositories/plugin-storage.js"; import type { Database } from "../../../src/database/types.js"; import { IdentifierError } from "../../../src/database/validate.js"; import { StorageQueryError } from "../../../src/plugins/storage-query.js"; import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js"; interface TestDocument { title: string; status: string; count: number; createdAt: string; } describe("PluginStorageRepository", () => { let db: Kysely; let repo: PluginStorageRepository; beforeEach(async () => { db = await setupTestDatabase(); repo = new PluginStorageRepository(db, "test-plugin", "items", [ "status", "count", ["status", "createdAt"], ]); }); afterEach(async () => { await teardownTestDatabase(db); }); describe("get()", () => { it("should return null for non-existent document", async () => { const result = await repo.get("non-existent"); expect(result).toBeNull(); }); it("should return document after put", async () => { const doc: TestDocument = { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }; await repo.put("doc1", doc); const result = await repo.get("doc1"); expect(result).toEqual(doc); }); }); describe("put()", () => { it("should store new document", async () => { const doc: TestDocument = { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }; await repo.put("doc1", doc); const result = await repo.get("doc1"); expect(result).toEqual(doc); }); it("should update existing document", async () => { const doc: TestDocument = { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }; await repo.put("doc1", doc); const updatedDoc = { ...doc, status: "inactive", count: 10 }; await repo.put("doc1", updatedDoc); const result = await repo.get("doc1"); expect(result).toEqual(updatedDoc); }); }); describe("delete()", () => { it("should return false for non-existent document", async () => { const result = await repo.delete("non-existent"); expect(result).toBe(false); }); it("should delete existing document and return true", async () => { await repo.put("doc1", { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }); const result = await repo.delete("doc1"); expect(result).toBe(true); const doc = await repo.get("doc1"); expect(doc).toBeNull(); }); }); describe("exists()", () => { it("should return false for non-existent document", async () => { const result = await repo.exists("non-existent"); expect(result).toBe(false); }); it("should return true for existing document", async () => { await repo.put("doc1", { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }); const result = await repo.exists("doc1"); expect(result).toBe(true); }); }); describe("getMany()", () => { it("should return empty map for empty ids", async () => { const result = await repo.getMany([]); expect(result.size).toBe(0); }); it("should return only existing documents", async () => { await repo.put("doc1", { title: "Test 1", status: "active", count: 1, createdAt: "2024-01-01", }); await repo.put("doc2", { title: "Test 2", status: "active", count: 2, createdAt: "2024-01-02", }); const result = await repo.getMany(["doc1", "doc2", "doc3"]); expect(result.size).toBe(2); expect(result.get("doc1")?.title).toBe("Test 1"); expect(result.get("doc2")?.title).toBe("Test 2"); expect(result.has("doc3")).toBe(false); }); }); describe("putMany()", () => { it("should handle empty array", async () => { await repo.putMany([]); // Should not throw }); it("should store multiple documents atomically", async () => { await repo.putMany([ { id: "doc1", data: { title: "Test 1", status: "active", count: 1, createdAt: "2024-01-01", }, }, { id: "doc2", data: { title: "Test 2", status: "inactive", count: 2, createdAt: "2024-01-02", }, }, ]); expect(await repo.exists("doc1")).toBe(true); expect(await repo.exists("doc2")).toBe(true); }); }); describe("deleteMany()", () => { it("should return 0 for empty ids", async () => { const count = await repo.deleteMany([]); expect(count).toBe(0); }); it("should delete multiple documents and return count", async () => { await repo.putMany([ { id: "doc1", data: { title: "Test 1", status: "active", count: 1, createdAt: "2024-01-01", }, }, { id: "doc2", data: { title: "Test 2", status: "active", count: 2, createdAt: "2024-01-02", }, }, { id: "doc3", data: { title: "Test 3", status: "active", count: 3, createdAt: "2024-01-03", }, }, ]); const count = await repo.deleteMany(["doc1", "doc2"]); expect(count).toBe(2); expect(await repo.exists("doc1")).toBe(false); expect(await repo.exists("doc2")).toBe(false); expect(await repo.exists("doc3")).toBe(true); }); }); describe("query()", () => { beforeEach(async () => { // Setup test data await repo.putMany([ { id: "doc1", data: { title: "Alpha", status: "active", count: 5, createdAt: "2024-01-01", }, }, { id: "doc2", data: { title: "Beta", status: "active", count: 10, createdAt: "2024-01-02", }, }, { id: "doc3", data: { title: "Gamma", status: "inactive", count: 15, createdAt: "2024-01-03", }, }, ]); }); it("should return all documents when no filter", async () => { const result = await repo.query(); expect(result.items).toHaveLength(3); }); it("should filter by equality", async () => { const result = await repo.query({ where: { status: "active" }, }); expect(result.items).toHaveLength(2); expect(result.items.every((i) => i.data.status === "active")).toBe(true); }); it("should filter by range (gte)", async () => { const result = await repo.query({ where: { count: { gte: 10 } }, }); expect(result.items).toHaveLength(2); expect(result.items.every((i) => i.data.count >= 10)).toBe(true); }); it("should filter by range (lt)", async () => { const result = await repo.query({ where: { count: { lt: 15 } }, }); expect(result.items).toHaveLength(2); expect(result.items.every((i) => i.data.count < 15)).toBe(true); }); it("should throw when querying non-indexed field", async () => { await expect( repo.query({ where: { title: "Alpha" }, }), ).rejects.toThrow(StorageQueryError); }); it("should reject malicious orderBy field names (SQL injection defense)", async () => { // Create a repo that declares a malicious index name to bypass the // "field must be indexed" check and hit the jsonExtract validation const evilRepo = new PluginStorageRepository(db, "test-plugin", "items", [ "'); DROP TABLE _plugin_storage--", ]); await expect( evilRepo.query({ orderBy: { "'); DROP TABLE _plugin_storage--": "asc" }, }), ).rejects.toThrow(IdentifierError); }); it("should respect limit", async () => { const result = await repo.query({ limit: 2 }); expect(result.items).toHaveLength(2); }); it("should provide cursor for pagination", async () => { const result = await repo.query({ limit: 2 }); expect(result.cursor).toBeDefined(); }); it("should not provide cursor when no more results", async () => { const result = await repo.query({ limit: 10 }); expect(result.cursor).toBeUndefined(); }); it("should paginate using cursor", async () => { const page1 = await repo.query({ limit: 2 }); expect(page1.items).toHaveLength(2); const page2 = await repo.query({ limit: 2, cursor: page1.cursor }); expect(page2.items).toHaveLength(1); expect(page2.cursor).toBeUndefined(); // Ensure no duplicates const allIds = [...page1.items, ...page2.items].map((i) => i.id); expect(new Set(allIds).size).toBe(3); }); }); describe("count()", () => { beforeEach(async () => { await repo.putMany([ { id: "doc1", data: { title: "Alpha", status: "active", count: 5, createdAt: "2024-01-01", }, }, { id: "doc2", data: { title: "Beta", status: "active", count: 10, createdAt: "2024-01-02", }, }, { id: "doc3", data: { title: "Gamma", status: "inactive", count: 15, createdAt: "2024-01-03", }, }, ]); }); it("should count all documents when no filter", async () => { const count = await repo.count(); expect(count).toBe(3); }); it("should count with filter", async () => { const count = await repo.count({ status: "active" }); expect(count).toBe(2); }); it("should return 0 for no matches", async () => { const count = await repo.count({ count: { gt: 100 } }); expect(count).toBe(0); }); it("should throw when counting on non-indexed field", async () => { await expect(repo.count({ title: "Alpha" })).rejects.toThrow(StorageQueryError); }); }); // Note: v2 API removed async iterator list() in favor of paginated query() // Use query() with cursor for iteration describe("plugin isolation", () => { it("should not see documents from other plugins", async () => { const otherRepo = new PluginStorageRepository(db, "other-plugin", "items", [ "status", ]); await repo.put("doc1", { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }); const result = await otherRepo.get("doc1"); expect(result).toBeNull(); }); it("should not see documents from other collections", async () => { const otherRepo = new PluginStorageRepository( db, "test-plugin", "other-collection", ["status"], ); await repo.put("doc1", { title: "Test", status: "active", count: 5, createdAt: "2024-01-01", }); const result = await otherRepo.get("doc1"); expect(result).toBeNull(); }); }); });