sotashimozono_obsidian-remo.../plugin/tests/SecretStore.test.ts
Souta 21bb70a5d0 chore: monorepo restructure (plugin/ + server/ + proto/)
Move the entire Obsidian plugin tree under plugin/ and add an empty
server/ tree (Go) and proto/ directory so the upcoming
obsidian-remote-server daemon has a home in the same repo. The plugin
remains the only thing that builds today; the server is a hello-world
skeleton that prints "not implemented yet" and exits non-zero, with a
Makefile that knows how to cross-compile for linux/darwin × amd64/arm64
once real code lands.

Background:
  Approach A (vault-adapter monkey-patch) hit structural ceilings —
  vault index frozen at startup, third-party plugins bypass DataAdapter,
  empty-remote .obsidian read failures cascade through Templater/Kanban
  etc. We are pivoting to the VS Code Remote-SSH model: a small Go
  daemon on the remote host, JSON-RPC over a SSH-tunnelled WebSocket,
  fsnotify-driven push events, attachment serving over HTTP on the same
  socket. This restructure is the first mechanical step before the
  protocol and transport land.

Mechanical changes:
- git mv all plugin sources, manifest, configs, scripts, and tests
  into plugin/.
- plugin/scripts/dev-install.mjs walks one extra parent (`pluginRoot`
  → `repoRoot` → `..` for the dev vault) so REMOTE_SSH_DEV_VAULT
  defaulting still resolves to ../SelfArchive-dev relative to the
  repo, not to plugin/.
- CI: ci.yml runs lint/test/build with `working-directory: plugin`
  and a server job that builds + tests the Go module. release.yml
  builds out of plugin/ and uploads plugin/{main.js,manifest.json,
  styles.css}.
- Dependabot: npm directory becomes /plugin; new gomod directory /server.
- Labeler: paths re-anchored to plugin/, with new plugin/server/proto
  area labels.

Verification:
  cd plugin && npm install && npx tsc --noEmit && npm test (76/76)
  cd plugin && npm run build:install (367 KB; dev vault refreshed).
  Go side waits on CI (no Go toolchain on the dev host).
2026-04-25 10:52:52 +09:00

72 lines
2.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { SecretStore } from '../src/ssh/SecretStore';
describe('SecretStore', () => {
let store: SecretStore;
beforeEach(() => { store = new SecretStore(); });
it('set and get round-trip', () => {
store.set('profile1:password', 'correct-horse-battery-staple');
expect(store.get('profile1:password')).toBe('correct-horse-battery-staple');
});
it('returns undefined for unknown ref', () => {
expect(store.get('nonexistent')).toBeUndefined();
});
it('has() returns true only for stored refs', () => {
store.set('key', 'value');
expect(store.has('key')).toBe(true);
expect(store.has('other')).toBe(false);
});
it('delete removes the entry', () => {
store.set('key', 'secret');
store.delete('key');
expect(store.get('key')).toBeUndefined();
expect(store.has('key')).toBe(false);
});
it('serialize + load round-trip preserves values', () => {
store.set('a', 'alpha');
store.set('b', 'beta');
const blob = store.serialize();
const store2 = new SecretStore();
store2.load(blob);
expect(store2.get('a')).toBe('alpha');
expect(store2.get('b')).toBe('beta');
});
it('different values produce different ciphertext', () => {
store.set('k1', 'secret-a');
store.set('k2', 'secret-b');
const blob = store.serialize();
expect(blob['k1'].data).not.toBe(blob['k2'].data);
});
it('same value encrypted twice produces different ciphertext (random IV)', () => {
const store2 = new SecretStore();
store.set('k', 'same-secret');
store2.set('k', 'same-secret');
const b1 = store.serialize();
const b2 = store2.serialize();
expect(b1['k'].iv).not.toBe(b2['k'].iv);
});
it('tampered data returns undefined gracefully', () => {
store.set('k', 'value');
const blob = store.serialize();
// Corrupt the ciphertext
blob['k'].data = 'deadbeef'.repeat(4);
const store2 = new SecretStore();
store2.load(blob);
expect(store2.get('k')).toBeUndefined();
});
it('load on undefined starts empty', () => {
store.load(undefined);
expect(store.has('anything')).toBe(false);
});
});