b695a37f5dd2

add vault password-history tests (capture, cap, restore, persistence) and message fill/edit history-stripping test

time
2026-06-06 19:30 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
password history
parent
e3936c5a967c
2 files src/background/messages.test.ts+22 −0src/background/vault.test.ts+106 −0
src/background/messages.test.ts+22 −0
@@ -840,6 +840,28 @@840840     expect(data.totpRemaining).toBeLessThanOrEqual(30);841841   });842842 843+  it("GET_ENTRY_FOR_FILL strips passwordHistory; GET_ENTRY_FOR_EDIT keeps it", async () => {844+    const vault = await import("./vault");845+    await vault.createVault("correct-pass-12345");846+    const e = await vault.addEntry({847+      title: "Bank",848+      username: "june",849+      password: "old-pw",850+      url: "https://bank.test",851+    });852+    await vault.updateEntry(e.id, { password: "new-pw" });853+854+    const fill = await dispatch({ type: "GET_ENTRY_FOR_FILL", payload: { id: e.id } });855+    expect(fill.ok).toBe(true);856+    // Passive autofill never receives prior passwords (blast-radius containment).857+    expect((fill.data as Record<string, unknown>).passwordHistory).toBeUndefined();858+859+    const edit = await dispatch({ type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } });860+    expect(edit.ok).toBe(true);861+    const editHist = (edit.data as { passwordHistory?: Array<{ password: string }> }).passwordHistory ?? [];862+    expect(editHist.map((h) => h.password)).toEqual(["old-pw"]);863+  });864+843865   it("GET_ENTRY_FOR_EDIT returns the raw TOTP seed for the trusted edit form", async () => {844866     const vault = await import("./vault");845867     await vault.createVault("correct-pass-12345");

view file

src/background/vault.test.ts+106 −0
@@ -1071,4 +1071,110 @@10711071     expect(vault.getEntryById(e.id)).toBeDefined();10721072     expect(vault.getDeletedEntries().find((t) => t.id === e.id)).toBeUndefined();10731073   });1074+1075+  describe("password history", () => {1076+    it("captures the old password on change, newest first", async () => {1077+      const vault = await import("./vault");1078+      await vault.createVault("history-pass-12345");1079+      const e = await vault.addEntry({ title: "H", username: "june", password: "first", url: "https://h.test" });1080+      expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();1081+1082+      await vault.updateEntry(e.id, { password: "second" });1083+      await vault.updateEntry(e.id, { password: "third" });1084+1085+      const hist = vault.getEntryById(e.id)?.passwordHistory ?? [];1086+      expect(hist.map((h) => h.password)).toEqual(["second", "first"]);1087+      expect(vault.getEntryById(e.id)?.password).toBe("third");1088+    });1089+1090+    it("does not record history on a metadata-only update", async () => {1091+      const vault = await import("./vault");1092+      await vault.createVault("history-meta-12345");1093+      const e = await vault.addEntry({ title: "M", username: "june", password: "keepme", url: "https://m.test" });1094+1095+      await vault.updateEntry(e.id, { title: "Renamed" });1096+      await vault.updateEntry(e.id, { username: "newuser" });1097+1098+      expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();1099+    });1100+1101+    it("records each history item with the age the password carried", async () => {1102+      const vault = await import("./vault");1103+      await vault.createVault("history-stamp-12345");1104+      const e = await vault.addEntry({ title: "S", username: "june", password: "v1", url: "https://s.test" });1105+      const firstChangedAt = vault.getEntryById(e.id)!.passwordChangedAt;1106+      expect(firstChangedAt).toBeGreaterThan(0);1107+1108+      await new Promise((resolve) => setTimeout(resolve, 2));1109+      await vault.updateEntry(e.id, { password: "v2" });1110+1111+      const hist = vault.getEntryById(e.id)!.passwordHistory!;1112+      // The retired "v1" entry carries the timestamp it held while current.1113+      expect(hist[0].changedAt).toBe(firstChangedAt);1114+    });1115+1116+    it("skips capture when re-saving the identical password", async () => {1117+      const vault = await import("./vault");1118+      await vault.createVault("history-noop-12345");1119+      const e = await vault.addEntry({ title: "N", username: "june", password: "same", url: "https://n.test" });1120+1121+      await vault.updateEntry(e.id, { password: "same" });1122+1123+      expect(vault.getEntryById(e.id)?.passwordHistory).toBeUndefined();1124+    });1125+1126+    it("does not capture an empty prior password (clear then set)", async () => {1127+      const vault = await import("./vault");1128+      await vault.createVault("history-empty-12345");1129+      const e = await vault.addEntry({ title: "E", username: "june", password: "", url: "https://e.test" });1130+1131+      await vault.updateEntry(e.id, { password: "now-has-one" });1132+1133+      // Nothing worth keeping when the prior value was empty.1134+      expect(vault.getEntryById(e.id)?.passwordHistory ?? []).toHaveLength(0);1135+    });1136+1137+    it("restoring an old password re-records the swapped-out current one", async () => {1138+      const vault = await import("./vault");1139+      await vault.createVault("history-restore-12345");1140+      const e = await vault.addEntry({ title: "R", username: "june", password: "old", url: "https://r.test" });1141+      await vault.updateEntry(e.id, { password: "new" });1142+1143+      // Restore "old" — the editor just saves the old value as the current one.1144+      await vault.updateEntry(e.id, { password: "old" });1145+1146+      const hist = vault.getEntryById(e.id)!.passwordHistory!;1147+      expect(vault.getEntryById(e.id)?.password).toBe("old");1148+      expect(hist.map((h) => h.password)).toEqual(["new", "old"]);1149+    });1150+1151+    it("preserves history across lock/unlock (normalizeEntry whitelist)", async () => {1152+      const vault = await import("./vault");1153+      await vault.createVault("history-persist-12345");1154+      const e = await vault.addEntry({ title: "P", username: "june", password: "p1", url: "https://p.test" });1155+      await vault.updateEntry(e.id, { password: "p2" });1156+1157+      vault.lock();1158+      expect(await vault.unlock("history-persist-12345")).toBe(true);1159+1160+      const hist = vault.getEntryById(e.id)?.passwordHistory ?? [];1161+      expect(hist.map((h) => h.password)).toEqual(["p1"]);1162+    });1163+1164+    it("caps history at 50 entries, dropping the oldest", async () => {1165+      const vault = await import("./vault");1166+      await vault.createVault("history-cap-12345");1167+      const e = await vault.addEntry({ title: "C", username: "june", password: "pw-0", url: "https://c.test" });1168+1169+      for (let i = 1; i <= 55; i++) {1170+        await vault.updateEntry(e.id, { password: `pw-${i}` });1171+      }1172+1173+      const hist = vault.getEntryById(e.id)!.passwordHistory!;1174+      expect(hist).toHaveLength(50);1175+      // Newest retired password is pw-54 (pw-55 is now current); oldest kept is pw-5.1176+      expect(hist[0].password).toBe("pw-54");1177+      expect(hist[hist.length - 1].password).toBe("pw-5");1178+    });1179+  });10741180 });

view file