Initial Commit
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { DriveLike } from "@suaveplan/vault";
|
||||
|
||||
/** Minimal in-memory `DriveLike` for tests. */
|
||||
export function createMemoryDrive(): DriveLike {
|
||||
const store = new Map<string, Uint8Array>();
|
||||
return {
|
||||
async read(path) {
|
||||
const bytes = store.get(path);
|
||||
if (bytes === undefined)
|
||||
throw new Error(`memory-drive: missing path ${path}`);
|
||||
return bytes;
|
||||
},
|
||||
async write(path, data) {
|
||||
store.set(path, data);
|
||||
},
|
||||
async delete(path) {
|
||||
return store.delete(path);
|
||||
},
|
||||
async exists(path) {
|
||||
return store.has(path);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Test setup for @suaveplan/api-plugin-vault.
|
||||
*
|
||||
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
|
||||
* module imports so the meter resolved on first call binds to it. Node 18+
|
||||
* + Bun expose `crypto.subtle` / `crypto.getRandomValues` globally so no
|
||||
* crypto polyfill is required.
|
||||
*/
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
AggregationTemporality,
|
||||
InMemoryMetricExporter,
|
||||
MeterProvider,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
|
||||
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
|
||||
AggregationTemporality.CUMULATIVE
|
||||
);
|
||||
|
||||
const reader = new PeriodicExportingMetricReader({
|
||||
exporter: testInMemoryMetricExporter,
|
||||
exportIntervalMillis: 60_000,
|
||||
});
|
||||
|
||||
export const testMeterProvider = new MeterProvider({ readers: [reader] });
|
||||
metrics.setGlobalMeterProvider(testMeterProvider);
|
||||
|
||||
export async function flushTestMetrics() {
|
||||
await testMeterProvider.forceFlush();
|
||||
const collected = testInMemoryMetricExporter.getMetrics();
|
||||
testInMemoryMetricExporter.reset();
|
||||
return collected;
|
||||
}
|
||||
Reference in New Issue
Block a user