import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- // We need to mock config BEFORE importing CookieStore, because the module // reads config.cookieDir at import time. let testDir: string; vi.mock('../src/config/index.js', () => ({ config: { get cookieDir() { return testDir; }, }, })); vi.mock('../src/utils/logger.js', () => ({ logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), child: vi.fn(() => ({ error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), })), }, })); // Import AFTER mocks are declared. import { CookieStore, type StorageState } from '../src/cookie/store.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeStorageState(cookieCount = 1): StorageState { const cookies = Array.from({ length: cookieCount }, (_, i) => ({ name: `cookie_${i}`, value: `value_${i}`, domain: '.example.com', path: '/', expires: Date.now() / 1000 + 3600, httpOnly: true, secure: true, sameSite: 'Lax' as const, })); return { cookies, origins: [], }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('CookieStore', () => { let store: CookieStore; beforeEach(async () => { testDir = path.join(os.tmpdir(), `cookie-store-test-${crypto.randomUUID()}`); await fs.mkdir(testDir, { recursive: true }); store = new CookieStore(); }); afterEach(async () => { await fs.rm(testDir, { recursive: true, force: true }); }); // -- save --------------------------------------------------------------- it('save creates the platform directory', async () => { const state = makeStorageState(); await store.save('twitter', state); const dirStat = await fs.stat(path.join(testDir, 'twitter')); expect(dirStat.isDirectory()).toBe(true); }); it('save writes the cookie file with restricted permissions (0o600)', async () => { const state = makeStorageState(); await store.save('twitter', state); const filePath = store.getPath('twitter'); const fileStat = await fs.stat(filePath); // On Unix-like systems the mode includes the file type bits; mask to // the permission bits only. const perms = fileStat.mode & 0o777; expect(perms).toBe(0o600); }); // -- load --------------------------------------------------------------- it('load returns saved data', async () => { const state = makeStorageState(3); await store.save('instagram', state); const loaded = await store.load('instagram'); expect(loaded).not.toBeNull(); expect(loaded!.cookies).toHaveLength(3); expect(loaded!.cookies[0]!.name).toBe('cookie_0'); }); it('load returns null for non-existent platform', async () => { const loaded = await store.load('nonexistent'); expect(loaded).toBeNull(); }); // -- delete ------------------------------------------------------------- it('delete removes the cookie file', async () => { const state = makeStorageState(); await store.save('weibo', state); // Verify the file exists first. const filePath = store.getPath('weibo'); await expect(fs.access(filePath)).resolves.toBeUndefined(); await store.delete('weibo'); // After deletion the file should no longer exist. await expect(fs.access(filePath)).rejects.toThrow(); }); it('delete succeeds silently for a non-existent file', async () => { // Should not throw even though no file was ever saved. await expect(store.delete('ghost')).resolves.toBeUndefined(); }); // -- atomic write ------------------------------------------------------- it('save uses atomic write (temp file renamed to final path)', async () => { const state = makeStorageState(); // Spy on fs.rename to verify it is called. const renameSpy = vi.spyOn(fs, 'rename'); await store.save('atomic-test', state); expect(renameSpy).toHaveBeenCalledTimes(1); const [tmpArg, finalArg] = renameSpy.mock.calls[0]!; expect(String(tmpArg)).toContain('.tmp.'); expect(String(finalArg)).toBe(store.getPath('atomic-test')); renameSpy.mockRestore(); }); });