























@@ -18,6 +18,7 @@ const hoisted = vi.hoisted(() => ({
1818waitForCredsSaveQueueWithTimeout: vi.fn<() => Promise<CredsQueueWaitResult>>(
1919async () => "drained",
2020),
21+oauthDir: "/tmp/openclaw-wa-auth-store-test-oauth",
2122}));
22232324vi.mock("./creds-persistence.js", async () => {
@@ -29,12 +30,31 @@ vi.mock("./creds-persistence.js", async () => {
2930};
3031});
313233+vi.mock("./auth-store.runtime.js", () => ({
34+resolveOAuthDir: () => hoisted.oauthDir,
35+}));
36+3237function createTempAuthDir(prefix: string) {
3338return fsSync.mkdtempSync(
3439path.join((process.env.TMPDIR ?? "/tmp").replace(/\/+$/, ""), `${prefix}-`),
3540);
3641}
374243+function withOwnedOAuthAuthDir<T>(
44+prefix: string,
45+run: (authDir: string) => Promise<T>,
46+): Promise<T> {
47+const previousOAuthDir = hoisted.oauthDir;
48+const oauthDir = createTempAuthDir(`${prefix}-oauth`);
49+const authDir = path.join(oauthDir, "whatsapp", "default");
50+fsSync.mkdirSync(authDir, { recursive: true });
51+hoisted.oauthDir = oauthDir;
52+return run(authDir).finally(() => {
53+hoisted.oauthDir = previousOAuthDir;
54+fsSync.rmSync(oauthDir, { recursive: true, force: true });
55+});
56+}
57+3858describe("auth-store", () => {
3959beforeEach(() => {
4060hoisted.waitForCredsSaveQueueWithTimeout.mockReset().mockResolvedValue("drained");
@@ -115,29 +135,32 @@ describe("auth-store", () => {
115135});
116136117137it("clears unreadable auth state on explicit logout", async () => {
118-const authDir = createTempAuthDir("openclaw-wa-auth-logout");
119-fsSync.writeFileSync(path.join(authDir, "creds.json"), "{", "utf-8");
120-fsSync.writeFileSync(
121-path.join(authDir, "creds.json.bak"),
122-JSON.stringify({ me: { id: "123@s.whatsapp.net" } }),
123-"utf-8",
124-);
138+await withOwnedOAuthAuthDir("openclaw-wa-auth-logout", async (authDir) => {
139+ fsSync.writeFileSync(path.join(authDir, "creds.json"), "{", "utf-8");
140+ fsSync.writeFileSync(
141+ path.join(authDir, "creds.json.bak"),
142+ JSON.stringify({ me: { id: "123@s.whatsapp.net" } }),
143+ "utf-8",
144+ );
125145126-const runtime = {
127-log: vi.fn(),
128-error: vi.fn(),
129-exit: vi.fn(),
130-};
146+ const runtime = {
147+ log: vi.fn(),
148+ error: vi.fn(),
149+ exit: vi.fn(),
150+ };
131151132-await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(true);
133-expect(fsSync.existsSync(authDir)).toBe(false);
152+await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(true);
153+expect(fsSync.existsSync(authDir)).toBe(false);
154+});
134155});
135156136157it("does not delete the whole legacy auth root when targeted cleanup fails", async () => {
137158const authDir = createTempAuthDir("openclaw-wa-auth-legacy-failure");
159+const previousOAuthDir = hoisted.oauthDir;
138160fsSync.writeFileSync(path.join(authDir, "creds.json"), "{}", "utf-8");
139161fsSync.writeFileSync(path.join(authDir, "oauth.json"), '{"token":true}', "utf-8");
140162fsSync.writeFileSync(path.join(authDir, "session-abc.json"), "{}", "utf-8");
163+hoisted.oauthDir = authDir;
141164const originalRm = fs.rm;
142165const rmSpy = vi.spyOn(fs, "rm").mockImplementation(async (target, options) => {
143166if (String(target).endsWith("creds.json")) {
@@ -151,29 +174,114 @@ describe("auth-store", () => {
151174exit: vi.fn(),
152175};
153176154-await expect(
155-logoutWeb({ authDir, isLegacyAuthDir: true, runtime: runtime as never }),
156-).rejects.toThrow("EACCES");
157-expect(fsSync.existsSync(authDir)).toBe(true);
158-expect(fsSync.existsSync(path.join(authDir, "oauth.json"))).toBe(true);
159-rmSpy.mockRestore();
177+try {
178+await expect(
179+logoutWeb({ authDir, isLegacyAuthDir: true, runtime: runtime as never }),
180+).rejects.toThrow("EACCES");
181+expect(fsSync.existsSync(authDir)).toBe(true);
182+expect(fsSync.existsSync(path.join(authDir, "oauth.json"))).toBe(true);
183+} finally {
184+hoisted.oauthDir = previousOAuthDir;
185+rmSpy.mockRestore();
186+fsSync.rmSync(authDir, { recursive: true, force: true });
187+}
160188});
161189162190it("clears auth state even when directory enumeration fails", async () => {
163-const authDir = createTempAuthDir("openclaw-wa-auth-readdir");
191+await withOwnedOAuthAuthDir("openclaw-wa-auth-readdir", async (authDir) => {
192+fsSync.writeFileSync(path.join(authDir, "creds.json"), "{}", "utf-8");
193+const readdirSpy = vi
194+.spyOn(fs, "readdir")
195+.mockRejectedValueOnce(Object.assign(new Error("EACCES"), { code: "EACCES" }));
196+const runtime = {
197+log: vi.fn(),
198+error: vi.fn(),
199+exit: vi.fn(),
200+};
201+202+await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(true);
203+expect(fsSync.existsSync(authDir)).toBe(false);
204+readdirSpy.mockRestore();
205+});
206+});
207+208+it("does not delete custom auth directories outside the OpenClaw auth root", async () => {
209+const authDir = createTempAuthDir("openclaw-wa-auth-custom");
210+const nestedDir = path.join(authDir, "nested");
211+fsSync.mkdirSync(nestedDir);
164212fsSync.writeFileSync(path.join(authDir, "creds.json"), "{}", "utf-8");
165-const readdirSpy = vi
166-.spyOn(fs, "readdir")
167-.mockRejectedValueOnce(Object.assign(new Error("EACCES"), { code: "EACCES" }));
213+fsSync.writeFileSync(path.join(authDir, "notes.txt"), "keep me", "utf-8");
214+fsSync.writeFileSync(path.join(nestedDir, "session-abc.json"), "keep me", "utf-8");
168215const runtime = {
169216log: vi.fn(),
170217error: vi.fn(),
171218exit: vi.fn(),
172219};
173220174-await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(true);
175-expect(fsSync.existsSync(authDir)).toBe(false);
176-readdirSpy.mockRestore();
221+await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(false);
222+expect(fsSync.existsSync(authDir)).toBe(true);
223+expect(fsSync.existsSync(path.join(authDir, "creds.json"))).toBe(true);
224+expect(fsSync.existsSync(path.join(authDir, "notes.txt"))).toBe(true);
225+expect(fsSync.existsSync(path.join(nestedDir, "session-abc.json"))).toBe(true);
226+});
227+228+it("does not clear auth files through a symlinked owned auth directory", async () => {
229+const previousOAuthDir = hoisted.oauthDir;
230+const oauthDir = createTempAuthDir("openclaw-wa-auth-symlink-oauth");
231+const externalDir = createTempAuthDir("openclaw-wa-auth-symlink-target");
232+const authDir = path.join(oauthDir, "whatsapp", "default");
233+try {
234+fsSync.mkdirSync(path.dirname(authDir), { recursive: true });
235+fsSync.writeFileSync(path.join(externalDir, "creds.json"), "{}", "utf-8");
236+fsSync.writeFileSync(path.join(externalDir, "notes.txt"), "keep me", "utf-8");
237+fsSync.symlinkSync(externalDir, authDir, "dir");
238+hoisted.oauthDir = oauthDir;
239+const runtime = {
240+log: vi.fn(),
241+error: vi.fn(),
242+exit: vi.fn(),
243+};
244+245+await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(false);
246+expect(fsSync.existsSync(authDir)).toBe(true);
247+expect(fsSync.existsSync(path.join(externalDir, "creds.json"))).toBe(true);
248+expect(fsSync.existsSync(path.join(externalDir, "notes.txt"))).toBe(true);
249+} finally {
250+hoisted.oauthDir = previousOAuthDir;
251+fsSync.rmSync(oauthDir, { recursive: true, force: true });
252+fsSync.rmSync(externalDir, { recursive: true, force: true });
253+}
254+});
255+256+it("does not clear auth files through an intermediate symlink in the owned auth tree", async () => {
257+const previousOAuthDir = hoisted.oauthDir;
258+const oauthDir = createTempAuthDir("openclaw-wa-auth-symlink-parent-oauth");
259+const externalRoot = createTempAuthDir("openclaw-wa-auth-symlink-parent-target");
260+const externalAuthDir = path.join(externalRoot, "default");
261+const linkedParent = path.join(oauthDir, "whatsapp", "linked");
262+const authDir = path.join(linkedParent, "default");
263+try {
264+fsSync.mkdirSync(path.dirname(linkedParent), { recursive: true });
265+fsSync.mkdirSync(externalAuthDir, { recursive: true });
266+fsSync.writeFileSync(path.join(externalAuthDir, "creds.json"), "{}", "utf-8");
267+fsSync.writeFileSync(path.join(externalAuthDir, "notes.txt"), "keep me", "utf-8");
268+fsSync.symlinkSync(externalRoot, linkedParent, "dir");
269+hoisted.oauthDir = oauthDir;
270+const runtime = {
271+log: vi.fn(),
272+error: vi.fn(),
273+exit: vi.fn(),
274+};
275+276+await expect(logoutWeb({ authDir, runtime: runtime as never })).resolves.toBe(false);
277+expect(fsSync.existsSync(authDir)).toBe(true);
278+expect(fsSync.existsSync(path.join(externalAuthDir, "creds.json"))).toBe(true);
279+expect(fsSync.existsSync(path.join(externalAuthDir, "notes.txt"))).toBe(true);
280+} finally {
281+hoisted.oauthDir = previousOAuthDir;
282+fsSync.rmSync(oauthDir, { recursive: true, force: true });
283+fsSync.rmSync(externalRoot, { recursive: true, force: true });
284+}
177285});
178286179287it("does not delete unrelated non-empty directories on logout", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。