129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import {
|
|
VAULT_PLUGIN_CODES,
|
|
VaultPluginError,
|
|
} from "../../src/error-codes/error-codes.js";
|
|
import type { VaultService } from "../../src/services/vault-service.js";
|
|
import type {
|
|
VaultHandle,
|
|
VaultItem,
|
|
VaultItemSummary,
|
|
} from "../../src/types/types.js";
|
|
|
|
/**
|
|
* Lightweight in-memory `VaultService` used by unit tests so route
|
|
* coverage does not pay for a real Argon2id pipeline. Stores items in a
|
|
* per-principal Map.
|
|
*/
|
|
export interface FakeVaultServiceOptions {
|
|
/** Maps principalId → expected password. Wrong password rejects. */
|
|
readonly passwords?: Record<string, string>;
|
|
}
|
|
|
|
export function createFakeVaultService(
|
|
options?: FakeVaultServiceOptions
|
|
): VaultService {
|
|
const passwords = options?.passwords ?? {};
|
|
const byPrincipal = new Map<string, Map<string, VaultItem>>();
|
|
const handles = new WeakMap<VaultHandle, { principalId: string }>();
|
|
let idCounter = 0;
|
|
function bucket(principalId: string): Map<string, VaultItem> {
|
|
let b = byPrincipal.get(principalId);
|
|
if (b === undefined) {
|
|
b = new Map();
|
|
byPrincipal.set(principalId, b);
|
|
}
|
|
return b;
|
|
}
|
|
function getState(handle: VaultHandle) {
|
|
const s = handles.get(handle);
|
|
if (s === undefined) {
|
|
throw new VaultPluginError(
|
|
VAULT_PLUGIN_CODES.FORBIDDEN,
|
|
"fake handle invalid",
|
|
{ context: { statusCode: 403 } }
|
|
);
|
|
}
|
|
return s;
|
|
}
|
|
return {
|
|
async openForPrincipal(principal, password) {
|
|
const expected = passwords[principal.id];
|
|
if (expected !== undefined && expected !== password) {
|
|
throw new VaultPluginError(
|
|
VAULT_PLUGIN_CODES.UNLOCK_FAILED,
|
|
"fake unlock failed",
|
|
{ context: { statusCode: 401 } }
|
|
);
|
|
}
|
|
const handle: VaultHandle = Object.freeze({
|
|
__brand: "VaultHandle" as const,
|
|
principalId: principal.id,
|
|
});
|
|
handles.set(handle, { principalId: principal.id });
|
|
return handle;
|
|
},
|
|
async listItemsForOwner(ownerId) {
|
|
const b = byPrincipal.get(ownerId);
|
|
if (b === undefined) return [];
|
|
const out: VaultItemSummary[] = [];
|
|
for (const i of b.values()) {
|
|
const { body: _b, ...rest } = i;
|
|
void _b;
|
|
out.push(rest);
|
|
}
|
|
return out;
|
|
},
|
|
async listItems(handle) {
|
|
const { principalId } = getState(handle);
|
|
return Array.from(bucket(principalId).values());
|
|
},
|
|
async readItem(handle, itemId) {
|
|
const { principalId } = getState(handle);
|
|
return bucket(principalId).get(itemId) ?? null;
|
|
},
|
|
async createItem(handle, input) {
|
|
const { principalId } = getState(handle);
|
|
idCounter += 1;
|
|
const now = new Date().toISOString();
|
|
const item: VaultItem = {
|
|
id: `item-${idCounter}`,
|
|
ownerId: principalId,
|
|
title: input.title,
|
|
kind: input.kind,
|
|
body: input.body,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...(input.metadata !== undefined
|
|
? { metadata: input.metadata }
|
|
: {}),
|
|
};
|
|
bucket(principalId).set(item.id, item);
|
|
return item;
|
|
},
|
|
async updateItem(handle, itemId, patch) {
|
|
const { principalId } = getState(handle);
|
|
const existing = bucket(principalId).get(itemId);
|
|
if (existing === undefined) return null;
|
|
const updated: VaultItem = {
|
|
...existing,
|
|
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
...(patch.kind !== undefined ? { kind: patch.kind } : {}),
|
|
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
...(patch.metadata !== undefined
|
|
? { metadata: patch.metadata }
|
|
: {}),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
bucket(principalId).set(itemId, updated);
|
|
return updated;
|
|
},
|
|
async deleteItem(handle, itemId) {
|
|
const { principalId } = getState(handle);
|
|
return bucket(principalId).delete(itemId);
|
|
},
|
|
async close(handle) {
|
|
handles.delete(handle);
|
|
},
|
|
};
|
|
}
|