fix: validate store directory exists before CLI operations #63
@@ -1442,3 +1442,131 @@ describe("schema put - invalid schema error handling", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Store validation - non-existent paths (Issue #55)", () => {
|
||||
test("E2E-55-1: get with non-existent store reports store not found", async () => {
|
||||
const fakePath = "/nonexistent/path/to/store";
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["get", "AAAAAAAAAAAAA"],
|
||||
fakePath,
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
expect(stderr).toContain(fakePath);
|
||||
expect(stderr).not.toContain("Node not found");
|
||||
});
|
||||
|
||||
test("E2E-55-2: has with non-existent store reports store not found", async () => {
|
||||
const fakePath = `/tmp/nonexistent-store-${Date.now()}`;
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["has", "BBBBBBBBBBBBB"],
|
||||
fakePath,
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-3: verify with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["verify", "CCCCCCCCCCCCC"],
|
||||
"/does/not/exist",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-4: schema get with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["schema", "get", "@string"],
|
||||
"/fake/path",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-5: schema list with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["schema", "list"],
|
||||
"/nonexistent/store",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-6: walk with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["walk", "DDDDDDDDDDDDD"],
|
||||
"/tmp/missing-store",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-7: cat with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["cat", "EEEEEEEEEEEEE"],
|
||||
"/no/such/dir",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-8: refs with non-existent store reports store not found", async () => {
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["refs", "FFFFFFFFFFFFF"],
|
||||
"/nonexistent",
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("Store not found");
|
||||
});
|
||||
|
||||
test("E2E-55-9: existing store works normally (regression)", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
const payloadFile = join(tmpStore, "test.json");
|
||||
writeFileSync(payloadFile, JSON.stringify("test"));
|
||||
|
||||
const { stdout: hash } = await runCli(
|
||||
["put", "@string", payloadFile],
|
||||
tmpStore,
|
||||
);
|
||||
const { exitCode, stdout, stderr } = await runCli(
|
||||
["get", hash.trim()],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
const node = JSON.parse(stdout);
|
||||
expect(node).toHaveProperty("type");
|
||||
expect(node).toHaveProperty("payload", "test");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("E2E-55-10: init creates directory (should not validate)", async () => {
|
||||
const newStore = join(tmpdir(), `new-store-${Date.now()}`);
|
||||
try {
|
||||
const { exitCode, stdout, stderr } = await runCli(["init"], newStore);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
expect(stdout.trim()).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
const { existsSync } = await import("node:fs");
|
||||
expect(existsSync(newStore)).toBe(true);
|
||||
} finally {
|
||||
rmSync(newStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,12 +112,26 @@ function readJsonFile(file: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
function openStore(): Store {
|
||||
return createFsStore(resolve(storePath));
|
||||
/**
|
||||
* Validate that the store directory exists.
|
||||
* Dies with a clear error message if not found.
|
||||
*/
|
||||
function validateStoreExists(path: string): void {
|
||||
if (!existsSync(path)) {
|
||||
die(`Store not found at ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openStore(shouldCreate = false): Store {
|
||||
const fullPath = resolve(storePath);
|
||||
if (!shouldCreate) {
|
||||
validateStoreExists(fullPath);
|
||||
}
|
||||
return createFsStore(fullPath);
|
||||
}
|
||||
|
||||
function openVarStore(): VariableStore {
|
||||
const store = openStore();
|
||||
const store = openStore(true);
|
||||
mkdirSync(resolve(storePath), { recursive: true });
|
||||
return createVariableStore(resolve(varDbPath), store);
|
||||
}
|
||||
@@ -127,9 +141,12 @@ function openVarStore(): VariableStore {
|
||||
* If the input starts with @, resolve it via bootstrap
|
||||
* Otherwise, return the hash as-is
|
||||
*/
|
||||
async function resolveTypeHash(typeHashOrAlias: string): Promise<Hash> {
|
||||
async function resolveTypeHash(
|
||||
typeHashOrAlias: string,
|
||||
shouldCreate = false,
|
||||
): Promise<Hash> {
|
||||
if (typeHashOrAlias.startsWith("@")) {
|
||||
const store = openStore();
|
||||
const store = openStore(shouldCreate);
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
const resolvedHash = builtinSchemas[typeHashOrAlias];
|
||||
if (!resolvedHash) {
|
||||
@@ -233,7 +250,7 @@ async function cmdInit(): Promise<void> {
|
||||
}
|
||||
|
||||
async function cmdBootstrap(): Promise<void> {
|
||||
const store = openStore();
|
||||
const store = openStore(true);
|
||||
const builtinSchemas = await bootstrap(store);
|
||||
const metaHash = builtinSchemas["@schema"];
|
||||
console.log(metaHash);
|
||||
@@ -243,7 +260,7 @@ async function cmdSchemaPut(args: string[]): Promise<void> {
|
||||
const file = args[0];
|
||||
if (!file) die("Usage: json-cas schema put <file.json>");
|
||||
const schema = readJsonFile(file) as JSONSchema;
|
||||
const store = openStore();
|
||||
const store = openStore(true);
|
||||
try {
|
||||
const hash = await putSchema(store, schema);
|
||||
console.log(hash);
|
||||
@@ -299,9 +316,9 @@ async function cmdPut(args: string[]): Promise<void> {
|
||||
const file = args[1];
|
||||
if (!typeHashOrAlias || !file)
|
||||
die("Usage: json-cas put <type-hash> <file.json>");
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias, true);
|
||||
const payload = readJsonFile(file);
|
||||
const store = openStore();
|
||||
const store = openStore(true);
|
||||
|
||||
// Check if schema exists
|
||||
const schema = getSchema(store, typeHash);
|
||||
@@ -405,7 +422,7 @@ async function cmdHash(args: string[]): Promise<void> {
|
||||
const file = args[1];
|
||||
if (!typeHashOrAlias || !file)
|
||||
die("Usage: json-cas hash <type-hash> <file.json>");
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias);
|
||||
const typeHash = await resolveTypeHash(typeHashOrAlias, true);
|
||||
const payload = readJsonFile(file);
|
||||
const hash = await computeHash(typeHash, payload);
|
||||
console.log(hash);
|
||||
|
||||
Reference in New Issue
Block a user