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
↓ peach-browser-b695a37f.tar.gzatlas at this save
files changed2 files
src/background/messages.test.ts+22 −0src/background/vault.test.ts+106 −0src/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");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 });