|
| 1 | +// Where the identity and the vault live. |
| 2 | +// |
| 3 | +// These used to be three different answers. The identity was under |
| 4 | +// `~/.logicsrc`, the CLI config beside it, and the credential store resolved |
| 5 | +// against `process.cwd()` — so the vault was wherever you were standing when |
| 6 | +// you ran the command. Running the CLI inside a git checkout wrote a directory |
| 7 | +// literally named `credentials/vault` into that repo's working tree: untracked, |
| 8 | +// unignored, one `git add -A` from being published. |
| 9 | +// |
| 10 | +// There is one vault per user, per machine. That is what these pin. |
| 11 | +import { describe, it, expect, beforeEach, afterEach } from "vitest"; |
| 12 | +import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs"; |
| 13 | +import { tmpdir } from "node:os"; |
| 14 | +import { join } from "node:path"; |
| 15 | + |
| 16 | +import { logicsrcHome, identityPath, legacyLogicsrcHome } from "./identity.js"; |
| 17 | +import { defaultCredentialHome } from "./store.js"; |
| 18 | + |
| 19 | +const ENV_KEYS = ["LOGICSRC_HOME", "XDG_CONFIG_HOME", "HOME", "LOGICSRC_CREDENTIAL_HOME", "LOGICSRC_IDENTITY_FILE"] as const; |
| 20 | + |
| 21 | +let saved: Record<string, string | undefined>; |
| 22 | +let sandbox: string; |
| 23 | + |
| 24 | +beforeEach(() => { |
| 25 | + saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); |
| 26 | + sandbox = mkdtempSync(join(tmpdir(), "logicsrc-paths-")); |
| 27 | + for (const k of ENV_KEYS) delete process.env[k]; |
| 28 | + process.env.HOME = sandbox; |
| 29 | +}); |
| 30 | + |
| 31 | +afterEach(() => { |
| 32 | + for (const [k, v] of Object.entries(saved)) { |
| 33 | + if (v === undefined) delete process.env[k]; |
| 34 | + else process.env[k] = v; |
| 35 | + } |
| 36 | + rmSync(sandbox, { recursive: true, force: true }); |
| 37 | +}); |
| 38 | + |
| 39 | +describe("logicsrc home", () => { |
| 40 | + it("defaults to ~/.config/logicsrc", () => { |
| 41 | + expect(logicsrcHome()).toBe(join(sandbox, ".config", "logicsrc")); |
| 42 | + }); |
| 43 | + |
| 44 | + it("honours XDG_CONFIG_HOME", () => { |
| 45 | + process.env.XDG_CONFIG_HOME = join(sandbox, "xdg"); |
| 46 | + expect(logicsrcHome()).toBe(join(sandbox, "xdg", "logicsrc")); |
| 47 | + }); |
| 48 | + |
| 49 | + it("lets LOGICSRC_HOME override everything", () => { |
| 50 | + process.env.LOGICSRC_HOME = join(sandbox, "explicit"); |
| 51 | + expect(logicsrcHome()).toBe(join(sandbox, "explicit")); |
| 52 | + }); |
| 53 | +}); |
| 54 | + |
| 55 | +describe("the credential store", () => { |
| 56 | + it("never resolves against the working directory", () => { |
| 57 | + // The regression this exists for. Whatever the cwd is, the vault is not |
| 58 | + // under it — a `.logicsrc/` appearing inside a project is the bug. |
| 59 | + const home = defaultCredentialHome(); |
| 60 | + expect(home).toBe(join(sandbox, ".config", "logicsrc", "credentials")); |
| 61 | + expect(home.startsWith(process.cwd())).toBe(false); |
| 62 | + }); |
| 63 | + |
| 64 | + it("is the same store no matter where the CLI is run from", () => { |
| 65 | + const before = defaultCredentialHome(); |
| 66 | + const elsewhere = mkdtempSync(join(tmpdir(), "logicsrc-cwd-")); |
| 67 | + const original = process.cwd(); |
| 68 | + try { |
| 69 | + process.chdir(elsewhere); |
| 70 | + expect(defaultCredentialHome()).toBe(before); |
| 71 | + } finally { |
| 72 | + process.chdir(original); |
| 73 | + rmSync(elsewhere, { recursive: true, force: true }); |
| 74 | + } |
| 75 | + }); |
| 76 | + |
| 77 | + it("still takes an explicit LOGICSRC_CREDENTIAL_HOME", () => { |
| 78 | + process.env.LOGICSRC_CREDENTIAL_HOME = join(sandbox, "vol", "creds"); |
| 79 | + expect(defaultCredentialHome()).toBe(join(sandbox, "vol", "creds")); |
| 80 | + }); |
| 81 | + |
| 82 | + it("sits beside the identity, under one home", () => { |
| 83 | + expect(defaultCredentialHome()).toBe(join(logicsrcHome(), "credentials")); |
| 84 | + expect(identityPath()).toBe(join(logicsrcHome(), "identity.json")); |
| 85 | + }); |
| 86 | +}); |
| 87 | + |
| 88 | +describe("migrating off ~/.logicsrc", () => { |
| 89 | + it("moves the old directory, keeping the identity key", () => { |
| 90 | + // The secret key is the whole account: losing it loses every team vault |
| 91 | + // the member was ever given. So this is a move, not a fresh start. |
| 92 | + const legacy = legacyLogicsrcHome(); |
| 93 | + mkdirSync(legacy, { recursive: true }); |
| 94 | + writeFileSync(join(legacy, "identity.json"), '{"keys":{"secretKey":"kept"}}'); |
| 95 | + |
| 96 | + const home = logicsrcHome(); |
| 97 | + expect(existsSync(legacy)).toBe(false); |
| 98 | + expect(JSON.parse(readFileSync(join(home, "identity.json"), "utf8")).keys.secretKey).toBe("kept"); |
| 99 | + }); |
| 100 | + |
| 101 | + it("leaves the old directory alone once the new one exists", () => { |
| 102 | + // Two directories both claiming to be the identity is the state where a |
| 103 | + // login writes one and a read finds the other. Whatever is already at the |
| 104 | + // new path wins; the legacy one is not merged over it. |
| 105 | + const legacy = legacyLogicsrcHome(); |
| 106 | + mkdirSync(legacy, { recursive: true }); |
| 107 | + writeFileSync(join(legacy, "identity.json"), '{"keys":{"secretKey":"old"}}'); |
| 108 | + const home = join(sandbox, ".config", "logicsrc"); |
| 109 | + mkdirSync(home, { recursive: true }); |
| 110 | + writeFileSync(join(home, "identity.json"), '{"keys":{"secretKey":"current"}}'); |
| 111 | + |
| 112 | + logicsrcHome(); |
| 113 | + expect(JSON.parse(readFileSync(join(home, "identity.json"), "utf8")).keys.secretKey).toBe("current"); |
| 114 | + expect(existsSync(legacy)).toBe(true); |
| 115 | + }); |
| 116 | + |
| 117 | + it("does nothing when there is no legacy directory", () => { |
| 118 | + const home = logicsrcHome(); |
| 119 | + expect(existsSync(legacyLogicsrcHome())).toBe(false); |
| 120 | + expect(home).toBe(join(sandbox, ".config", "logicsrc")); |
| 121 | + }); |
| 122 | +}); |
0 commit comments