mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: persist cookies in app (#5318)
This commit is contained in:
43
e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
Normal file
43
e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {
|
||||
// Create a temporary user-data directory so we control where the cookies store file is written.
|
||||
const userDataPath = await createTmpDir('cookie-persistence');
|
||||
|
||||
const app1 = await launchElectronApp({ userDataPath });
|
||||
const page1 = await app1.firstWindow();
|
||||
await page1.waitForSelector('[data-trigger="cookies"]');
|
||||
|
||||
// Open Cookies modal via the status-bar button.
|
||||
await page1.click('[data-trigger="cookies"]');
|
||||
|
||||
// When no cookies are present the modal shows a centred "Add Cookie" button.
|
||||
await page1.getByRole('button', { name: /Add Cookie/i }).click();
|
||||
|
||||
// Fill out the form.
|
||||
await page1.fill('input[name="domain"]', 'example.com');
|
||||
await page1.fill('input[name="path"]', '/');
|
||||
await page1.fill('input[name="key"]', 'session');
|
||||
await page1.fill('input[name="value"]', 'abc123');
|
||||
await page1.check('input[name="secure"]');
|
||||
await page1.check('input[name="httpOnly"]');
|
||||
|
||||
await page1.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app1.close();
|
||||
|
||||
// Second launch – verify the cookie was persisted and re-loaded
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await app2.firstWindow();
|
||||
|
||||
// Open the Cookies modal again.
|
||||
await page2.waitForSelector('[data-trigger="cookies"]');
|
||||
await page2.click('[data-trigger="cookies"]');
|
||||
|
||||
// The domain we added earlier should still be present.
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app2.close();
|
||||
});
|
||||
47
e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
Normal file
47
e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
test('should handle corrupted passkey and still display saved cookie list', async ({ createTmpDir, launchElectronApp }) => {
|
||||
const userDataPath = await createTmpDir('corrupted-passkey');
|
||||
|
||||
const app1 = await launchElectronApp({ userDataPath });
|
||||
// 1. First run – add a cookie via the UI so `cookies.json` is created.
|
||||
const page1 = await app1.firstWindow();
|
||||
|
||||
await page1.waitForSelector('[data-trigger="cookies"]');
|
||||
await page1.click('[data-trigger="cookies"]');
|
||||
await page1.getByRole('button', { name: /Add Cookie/i }).click();
|
||||
|
||||
await page1.fill('input[name="domain"]', 'example.com');
|
||||
await page1.fill('input[name="path"]', '/');
|
||||
await page1.fill('input[name="key"]', 'session');
|
||||
await page1.fill('input[name="value"]', 'abc123');
|
||||
await page1.check('input[name="secure"]');
|
||||
await page1.check('input[name="httpOnly"]');
|
||||
|
||||
await page1.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app1.close();
|
||||
|
||||
// 2. Corrupt the encryptedPasskey in cookies.json
|
||||
const cookiesFilePath = path.join(userDataPath, 'cookies.json');
|
||||
const raw = await fs.readFile(cookiesFilePath, 'utf-8');
|
||||
const cookiesJson = JSON.parse(raw);
|
||||
cookiesJson.encryptedPasskey = 'deadbeef'; // clearly invalid value
|
||||
await fs.writeFile(cookiesFilePath, JSON.stringify(cookiesJson, null, 2));
|
||||
|
||||
// 3. Second run – Bruno should recover and still list the cookie domain
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await app2.firstWindow();
|
||||
|
||||
await page2.waitForSelector('[data-trigger="cookies"]');
|
||||
await page2.click('[data-trigger="cookies"]');
|
||||
|
||||
// The domain row should still be visible (even if cookie values are blank).
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app2.close();
|
||||
});
|
||||
@@ -32,6 +32,8 @@ const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window')
|
||||
const registerNotificationsIpc = require('./ipc/notifications');
|
||||
const registerGlobalEnvironmentsIpc = require('./ipc/global-environments');
|
||||
const { safeParseJSON, safeStringifyJSON } = require('./utils/common');
|
||||
const { getDomainsWithCookies } = require('./utils/cookies');
|
||||
const { cookiesStore } = require('./store/cookies');
|
||||
|
||||
const lastOpenedCollections = new LastOpenedCollections();
|
||||
|
||||
@@ -179,7 +181,7 @@ app.on('ready', async () => {
|
||||
mainWindow.minimize();
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
mainWindow.webContents.on('did-finish-load', async () => {
|
||||
let ogSend = mainWindow.webContents.send;
|
||||
mainWindow.webContents.send = function(channel, ...args) {
|
||||
return ogSend.apply(this, [channel, ...args?.map(_ => {
|
||||
@@ -187,6 +189,14 @@ app.on('ready', async () => {
|
||||
return safeParseJSON(safeStringifyJSON(_));
|
||||
})]);
|
||||
}
|
||||
// Send cookies list after renderer is ready
|
||||
try {
|
||||
cookiesStore.initializeCookies();
|
||||
const cookiesList = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', cookiesList);
|
||||
} catch (err) {
|
||||
console.error('Failed to load cookies for renderer', err);
|
||||
}
|
||||
});
|
||||
|
||||
// register all ipc handlers
|
||||
@@ -198,6 +208,14 @@ app.on('ready', async () => {
|
||||
});
|
||||
|
||||
// Quit the app once all windows are closed
|
||||
app.on('before-quit', () => {
|
||||
try {
|
||||
cookiesStore.saveCookieJar(true);
|
||||
} catch (err) {
|
||||
console.warn('Failed to flush cookies on quit', err);
|
||||
}
|
||||
});
|
||||
|
||||
app.on('window-all-closed', app.quit);
|
||||
|
||||
// Open collection from Recent menu (#1521)
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
} = require('@usebruno/filestore');
|
||||
const brunoConverters = require('@usebruno/converters');
|
||||
const { postmanToBruno } = brunoConverters;
|
||||
const { cookiesStore } = require('../store/cookies');
|
||||
const { parseLargeRequestWithRedaction } = require('../utils/parse');
|
||||
|
||||
const {
|
||||
@@ -890,12 +891,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
const updateCookiesAndNotify = async () => {
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send(
|
||||
'main:cookies-update',
|
||||
safeParseJSON(safeStringifyJSON(domainsWithCookies))
|
||||
);
|
||||
cookiesStore.saveCookieJar();
|
||||
};
|
||||
|
||||
// Delete all cookies for a domain
|
||||
ipcMain.handle('renderer:delete-cookies-for-domain', async (event, domain) => {
|
||||
try {
|
||||
await deleteCookiesForDomain(domain);
|
||||
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
await updateCookiesAndNotify();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -904,8 +913,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:delete-cookie', async (event, domain, path, cookieKey) => {
|
||||
try {
|
||||
await deleteCookie(domain, path, cookieKey);
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
await updateCookiesAndNotify();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -915,8 +923,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => {
|
||||
try {
|
||||
await addCookieForDomain(domain, cookie);
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
await updateCookiesAndNotify();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -926,8 +933,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
ipcMain.handle('renderer:modify-cookie', async (event, domain, oldCookie, cookie) => {
|
||||
try {
|
||||
await modifyCookieForDomain(domain, oldCookie, cookie);
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
await updateCookiesAndNotify();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
const { isRequestTagsIncluded } = require('@usebruno/common');
|
||||
const { cookiesStore } = require('../../store/cookies');
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
@@ -771,6 +772,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
@@ -900,6 +902,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
cookiesStore.saveCookieJar();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
192
packages/bruno-electron/src/store/cookies.js
Normal file
192
packages/bruno-electron/src/store/cookies.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const Store = require('electron-store');
|
||||
const { cookies: cookiesModule } = require('@usebruno/common');
|
||||
const { cookieJar } = cookiesModule;
|
||||
const { Cookie } = require('tough-cookie');
|
||||
const { createCookieString } = cookiesModule;
|
||||
const crypto = require('crypto');
|
||||
const { encryptString, decryptString } = require('../utils/encryption');
|
||||
|
||||
const DEBOUNCE_MS = 5000; // Debounce duration (ms) for persisting cookie jar
|
||||
class CookiesStore {
|
||||
#saveTimerId = null;
|
||||
#debounceStart = null; // Track first debounce time
|
||||
#passkey = null;
|
||||
|
||||
constructor() {
|
||||
this.store = new Store({
|
||||
name: 'cookies',
|
||||
clearInvalidConfig: true,
|
||||
defaults: {
|
||||
encryptedPasskey: null,
|
||||
cookies: {}
|
||||
}
|
||||
});
|
||||
this.initializeEncryption();
|
||||
}
|
||||
|
||||
#generatePasskey() {
|
||||
// Generate 32 bytes (256 bits) of random data and convert to hex
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
initializeEncryption() {
|
||||
try {
|
||||
let encryptedPasskey = this.store.get('encryptedPasskey');
|
||||
if (!encryptedPasskey) {
|
||||
// Generate cryptographically secure random passkey
|
||||
const passkey = this.#generatePasskey();
|
||||
encryptedPasskey = encryptString(passkey);
|
||||
if (!encryptedPasskey) {
|
||||
console.warn('Failed to encrypt new passkey, falling back to unencrypted cookies');
|
||||
this.#passkey = null;
|
||||
return;
|
||||
}
|
||||
this.store.set('encryptedPasskey', encryptedPasskey);
|
||||
}
|
||||
this.#passkey = decryptString(encryptedPasskey);
|
||||
if (!this.#passkey) {
|
||||
console.warn('Failed to decrypt passkey, falling back to unencrypted cookies');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to initialize encryption, falling back to unencrypted cookies:', err);
|
||||
this.#passkey = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCookies() {
|
||||
const cookieStore = this.store.get('cookies', {});
|
||||
const decryptedCookies = [];
|
||||
|
||||
// Filter and decrypt cookies
|
||||
Object.values(cookieStore).forEach(domainCookies => {
|
||||
if (!Array.isArray(domainCookies)) return;
|
||||
|
||||
domainCookies.forEach(cookie => {
|
||||
try {
|
||||
|
||||
// Create cookie with decrypted value
|
||||
const decryptedCookie = {
|
||||
...cookie,
|
||||
value: decryptString(cookie.value, this.#passkey)
|
||||
};
|
||||
decryptedCookies.push(decryptedCookie);
|
||||
} catch (err) {
|
||||
console.warn('Failed to process cookie:', cookie?.key, err);
|
||||
// Still add the cookie but with empty value if processing fails
|
||||
decryptedCookies.push({
|
||||
...cookie,
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return decryptedCookies;
|
||||
}
|
||||
|
||||
setCookies(cookies) {
|
||||
try {
|
||||
// Organize cookies by domain
|
||||
const cookiesByDomain = {};
|
||||
cookies.cookies.forEach(cookie => {
|
||||
try {
|
||||
if (!cookiesByDomain[cookie.domain]) {
|
||||
cookiesByDomain[cookie.domain] = [];
|
||||
}
|
||||
|
||||
cookiesByDomain[cookie.domain].push({
|
||||
...cookie,
|
||||
value: encryptString(cookie.value, this.#passkey)
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Failed to process cookie for storage:', cookie?.key, err);
|
||||
// Still store the cookie but with original value if encryption fails
|
||||
if (!cookiesByDomain[cookie.domain]) {
|
||||
cookiesByDomain[cookie.domain] = [];
|
||||
}
|
||||
cookiesByDomain[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
return this.store.set('cookies', cookiesByDomain);
|
||||
} catch (err) {
|
||||
console.warn('Failed to set cookies:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cookies from store into cookie jar
|
||||
initializeCookies() {
|
||||
try {
|
||||
const storedCookies = this.getCookies();
|
||||
|
||||
if (Array.isArray(storedCookies) && storedCookies.length) {
|
||||
storedCookies.forEach((cookie) => this.loadCookieIntoJar(cookie));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to initialize cookies:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load a single cookie into the cookie jar
|
||||
loadCookieIntoJar(rawCookie) {
|
||||
try {
|
||||
const cookie = Cookie.fromJSON(rawCookie);
|
||||
if (!cookie) return;
|
||||
|
||||
// Re-assemble request URL for tough-cookie
|
||||
const protocol = cookie.secure ? 'https' : 'http';
|
||||
const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain;
|
||||
const url = `${protocol}://${domain}${cookie.path || '/'}`;
|
||||
const setCookieHeader = createCookieString(cookie);
|
||||
|
||||
cookieJar.setCookieSync(setCookieHeader, url, { ignoreError: true });
|
||||
} catch (err) {
|
||||
console.warn('Failed to load cookie:', rawCookie?.key, err?.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Save current cookie jar state to store with debouncing
|
||||
writeCookieJar() {
|
||||
try {
|
||||
const serialized = cookieJar.serializeSync();
|
||||
this.setCookies(serialized);
|
||||
|
||||
} catch (err) {
|
||||
console.warn('Failed to save cookie jar:', err);
|
||||
} finally {
|
||||
this.#debounceStart = null;
|
||||
}
|
||||
}
|
||||
|
||||
saveCookieJar(immediate = false) {
|
||||
// Debounced write to avoid excessive disk I/O during rapid request bursts
|
||||
if (immediate) {
|
||||
if (this.#saveTimerId) {
|
||||
clearTimeout(this.#saveTimerId);
|
||||
this.#saveTimerId = null;
|
||||
}
|
||||
return this.writeCookieJar();
|
||||
}
|
||||
|
||||
if (!this.#debounceStart) {
|
||||
this.#debounceStart = Date.now();
|
||||
}
|
||||
|
||||
if (this.#saveTimerId) {
|
||||
clearTimeout(this.#saveTimerId);
|
||||
}
|
||||
this.#saveTimerId = setTimeout(() => {
|
||||
this.writeCookieJar();
|
||||
this.#saveTimerId = null;
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const cookiesStore = new CookiesStore();
|
||||
|
||||
module.exports = {
|
||||
cookiesStore,
|
||||
CookiesStore
|
||||
};
|
||||
@@ -29,8 +29,8 @@ function deriveKeyAndIv(password, keyLength, ivLength) {
|
||||
return { key, iv };
|
||||
}
|
||||
|
||||
function aes256Encrypt(data) {
|
||||
const rawKey = machineIdSync();
|
||||
function aes256Encrypt(data, passkey = null) {
|
||||
const rawKey = passkey || machineIdSync();
|
||||
const iv = Buffer.alloc(16, 0); // Default IV for new encryption
|
||||
const key = crypto.createHash('sha256').update(rawKey).digest(); // Derive a 32-byte key
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
@@ -40,8 +40,8 @@ function aes256Encrypt(data) {
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
function aes256Decrypt(data) {
|
||||
const rawKey = machineIdSync();
|
||||
function aes256Decrypt(data, passkey = null) {
|
||||
const rawKey = passkey || machineIdSync();
|
||||
|
||||
// Attempt to decrypt using new method first
|
||||
const iv = Buffer.alloc(16, 0); // Default IV for new encryption
|
||||
@@ -95,7 +95,7 @@ function safeStorageDecrypt(str) {
|
||||
}
|
||||
}
|
||||
|
||||
function encryptString(str) {
|
||||
function encryptString(str, passkey = null) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Encrypt failed: invalid string');
|
||||
}
|
||||
@@ -103,20 +103,31 @@ function encryptString(str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let encryptedString = '';
|
||||
// If a passkey is provided (from cookies store), we must use it for encryption.
|
||||
if (passkey !== null && passkey !== undefined) {
|
||||
if (typeof passkey !== 'string' || passkey.length === 0) {
|
||||
// Corrupted / empty passkey -> do not encrypt, return empty value
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const encryptedString = aes256Encrypt(str, passkey);
|
||||
return `$${AES256_ALGO}:${encryptedString}`;
|
||||
} catch (err) {
|
||||
// Any error indicates the passkey is unusable; return empty string
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (safeStorage && safeStorage.isEncryptionAvailable()) {
|
||||
encryptedString = safeStorageEncrypt(str);
|
||||
const encryptedString = safeStorageEncrypt(str);
|
||||
return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`;
|
||||
}
|
||||
|
||||
// fallback to aes256
|
||||
encryptedString = aes256Encrypt(str);
|
||||
|
||||
const encryptedString = aes256Encrypt(str);
|
||||
return `$${AES256_ALGO}:${encryptedString}`;
|
||||
}
|
||||
|
||||
function decryptString(str) {
|
||||
function decryptString(str, passkey = null) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new Error('Decrypt failed: unrecognized string format');
|
||||
}
|
||||
@@ -148,8 +159,9 @@ function decryptString(str) {
|
||||
}
|
||||
|
||||
if (algo === AES256_ALGO) {
|
||||
return aes256Decrypt(encryptedString);
|
||||
return aes256Decrypt(encryptedString, passkey || null);
|
||||
}
|
||||
throw new Error('Decrypt failed: Invalid algo');
|
||||
}
|
||||
|
||||
function decryptStringSafe(str) {
|
||||
|
||||
71
packages/bruno-electron/tests/cookies-store.test.js
Normal file
71
packages/bruno-electron/tests/cookies-store.test.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const path = require('path');
|
||||
|
||||
const mockEncrypt = (str) => (str.length ? `$enc:${str}` : '');
|
||||
const mockDecrypt = (str) => str.replace(/^\$enc:/, '');
|
||||
|
||||
jest.mock('../src/utils/encryption', () => ({
|
||||
encryptString: jest.fn(mockEncrypt),
|
||||
decryptString: jest.fn(mockDecrypt)
|
||||
}));
|
||||
|
||||
jest.mock('electron-store', () => {
|
||||
return jest.fn().mockImplementation((opts = {}) => {
|
||||
const data = { ...(opts.defaults || {}) };
|
||||
return {
|
||||
get: (key, fallback) => (key in data ? data[key] : fallback),
|
||||
set: (key, value) => {
|
||||
data[key] = value;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const { CookiesStore } = require(path.join('..', 'src', 'store', 'cookies'));
|
||||
|
||||
function createFreshStore() {
|
||||
return new CookiesStore();
|
||||
}
|
||||
|
||||
describe('CookiesStore', () => {
|
||||
test('setCookies encrypts values and getCookies returns decrypted values', () => {
|
||||
const store = createFreshStore();
|
||||
|
||||
const cookie = {
|
||||
domain: 'example.com',
|
||||
key: 'auth',
|
||||
value: 'token',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true
|
||||
};
|
||||
|
||||
store.setCookies({ cookies: [cookie] });
|
||||
|
||||
// Raw persisted value should be encrypted
|
||||
const raw = store.store.get('cookies');
|
||||
expect(raw['example.com'][0].value).toBe(`$enc:${cookie.value}`);
|
||||
|
||||
// API should return decrypted value
|
||||
const retrieved = store.getCookies();
|
||||
expect(retrieved[0].value).toBe(cookie.value);
|
||||
});
|
||||
|
||||
test('getCookies leaves plain-text cookie values untouched', () => {
|
||||
const store = createFreshStore();
|
||||
|
||||
const plainCookie = {
|
||||
domain: 'example.com',
|
||||
key: 'sid',
|
||||
value: 'plaintext',
|
||||
path: '/',
|
||||
secure: false,
|
||||
httpOnly: false
|
||||
};
|
||||
|
||||
// Manually inject to the underlying store to simulate legacy/plain data
|
||||
store.store.set('cookies', { 'example.com': [plainCookie] });
|
||||
|
||||
const cookies = store.getCookies();
|
||||
expect(cookies[0].value).toBe('plaintext');
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,9 @@ export const test = baseTest.extend<
|
||||
},
|
||||
{
|
||||
createTmpDir: (tag?: string) => Promise<string>;
|
||||
launchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
|
||||
launchElectronApp: (options?: { initUserDataPath?: string; userDataPath?: string }) => Promise<ElectronApplication>;
|
||||
electronApp: ElectronApplication;
|
||||
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
|
||||
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string; userDataPath?: string }) => Promise<ElectronApplication>;
|
||||
}
|
||||
>({
|
||||
createTmpDir: [
|
||||
@@ -37,8 +37,13 @@ export const test = baseTest.extend<
|
||||
launchElectronApp: [
|
||||
async ({ playwright, createTmpDir }, use, workerInfo) => {
|
||||
const apps: ElectronApplication[] = [];
|
||||
await use(async ({ initUserDataPath } = {}) => {
|
||||
const userDataPath = await createTmpDir('electron-userdata');
|
||||
await use(async ({ initUserDataPath, userDataPath: providedUserDataPath } = {}) => {
|
||||
const userDataPath = providedUserDataPath || (await createTmpDir('electron-userdata'));
|
||||
|
||||
// Ensure dir exists when caller supplies their own path
|
||||
if (providedUserDataPath) {
|
||||
await fs.promises.mkdir(userDataPath, { recursive: true });
|
||||
}
|
||||
|
||||
if (initUserDataPath) {
|
||||
const replacements = {
|
||||
@@ -67,10 +72,10 @@ export const test = baseTest.extend<
|
||||
});
|
||||
|
||||
const { workerIndex } = workerInfo;
|
||||
app.process().stdout.on('data', (data) => {
|
||||
app.process()?.stdout?.on('data', (data) => {
|
||||
process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
|
||||
});
|
||||
app.process().stderr.on('data', (error) => {
|
||||
app.process()?.stderr?.on('data', (error) => {
|
||||
process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
|
||||
});
|
||||
|
||||
@@ -137,13 +142,15 @@ export const test = baseTest.extend<
|
||||
reuseOrLaunchElectronApp: [
|
||||
async ({ launchElectronApp }, use, testInfo) => {
|
||||
const apps: Record<string, ElectronApplication> = {};
|
||||
await use(async ({ initUserDataPath } = {}) => {
|
||||
const key = initUserDataPath;
|
||||
await use(async ({ initUserDataPath, userDataPath } = {}) => {
|
||||
const key = userDataPath || initUserDataPath;
|
||||
if (key && apps[key]) {
|
||||
return apps[key];
|
||||
}
|
||||
const app = await launchElectronApp({ initUserDataPath });
|
||||
apps[key] = app;
|
||||
const app = await launchElectronApp({ initUserDataPath, userDataPath });
|
||||
if (key) {
|
||||
apps[key] = app;
|
||||
}
|
||||
return app;
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user