From 55774a82589cbc470e5dc5db13f3c3faf3255ace Mon Sep 17 00:00:00 2001 From: Pooja Date: Mon, 18 May 2026 12:52:10 +0530 Subject: [PATCH] fix: restore saved credentials when switching back to original auth mode (#7911) --- .../ReduxStore/slices/collections/index.js | 30 ++++-- .../auth-mode-switch-draft-indicator.spec.ts | 66 ++++++++++++ tests/auth/auth-mode-switch.spec.ts | 101 ++++++++++++++++++ tests/utils/page/actions.ts | 58 +++++++++- 4 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 tests/auth/auth-mode-switch-draft-indicator.spec.ts create mode 100644 tests/auth/auth-mode-switch.spec.ts diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index ed6cf95c8..7656990b8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1683,8 +1683,14 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.auth = {}; - item.draft.request.auth.mode = action.payload.mode; + const newMode = action.payload.mode; + const savedAuth = get(item, 'request.auth'); + const savedMode = get(savedAuth, 'mode'); + if (newMode === savedMode) { + item.draft.request.auth = cloneDeep(savedAuth); + } else { + item.draft.request.auth = { mode: newMode }; + } } } }, @@ -2113,8 +2119,14 @@ export const collectionsSlice = createSlice({ root: cloneDeep(collection.root) }; } - set(collection, 'draft.root.request.auth', {}); - set(collection, 'draft.root.request.auth.mode', action.payload.mode); + const newMode = action.payload.mode; + const savedAuth = get(collection, 'root.request.auth'); + const savedMode = get(savedAuth, 'mode'); + if (newMode === savedMode) { + set(collection, 'draft.root.request.auth', cloneDeep(savedAuth)); + } else { + set(collection, 'draft.root.request.auth', { mode: newMode }); + } } }, updateCollectionAuth: (state, action) => { @@ -3322,8 +3334,14 @@ export const collectionsSlice = createSlice({ if (!folder.draft) { folder.draft = cloneDeep(folder.root); } - set(folder, 'draft.request.auth', {}); - set(folder, 'draft.request.auth.mode', action.payload.mode); + const newMode = action.payload.mode; + const savedAuth = get(folder, 'root.request.auth'); + const savedMode = get(savedAuth, 'mode'); + if (newMode === savedMode) { + set(folder, 'draft.request.auth', cloneDeep(savedAuth)); + } else { + set(folder, 'draft.request.auth', { mode: newMode }); + } } }, streamDataReceived: (state, action) => { diff --git a/tests/auth/auth-mode-switch-draft-indicator.spec.ts b/tests/auth/auth-mode-switch-draft-indicator.spec.ts new file mode 100644 index 000000000..08f295f48 --- /dev/null +++ b/tests/auth/auth-mode-switch-draft-indicator.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '../../playwright'; +import { + closeAllCollections, + createCollection, + createRequest, + openRequest, + readField, + saveRequest, + selectAuthMode, + selectRequestPaneTab, + typeIntoField +} from '../utils/page'; + +type CollectionFormat = 'bru' | 'yml'; + +const runDraftIndicatorScenario = (format: CollectionFormat) => { + test(`(${format}) switching back to the saved auth mode hides the draft indicator`, async ({ page, createTmpDir }) => { + const collectionName = `auth-draft-indicator-${format}`; + const requestName = `request-${format}`; + + await createCollection(page, collectionName, await createTmpDir(), format); + await createRequest(page, requestName, collectionName, { url: 'https://example.com/api' }); + await openRequest(page, collectionName, requestName); + await selectRequestPaneTab(page, 'Auth'); + + const requestTab = page + .locator('.request-tab') + .filter({ has: page.locator('.tab-label', { hasText: requestName }) }); + + await test.step('Save Bearer with a token — draft indicator clears', async () => { + await selectAuthMode(page, 'Bearer Token'); + await typeIntoField(page, 'Token', 'saved-bearer-token'); + await saveRequest(page); + + await expect(requestTab.locator('.close-icon')).toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + + await test.step('Switching to Basic Auth without saving shows the draft indicator', async () => { + await selectAuthMode(page, 'Basic Auth'); + + await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); + await expect(requestTab.locator('.close-icon')).not.toBeVisible(); + }); + + await test.step('Switching back to the saved Bearer mode without saving hides the draft indicator', async () => { + await selectAuthMode(page, 'Bearer Token'); + + // Saved token is restored + await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token'); + + // Draft now deep-equals the saved state — indicator must be gone + await expect(requestTab.locator('.close-icon')).toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + }); +}; + +test.describe('Auth mode switch — draft indicator clears on return to saved mode', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + + runDraftIndicatorScenario('bru'); + runDraftIndicatorScenario('yml'); +}); diff --git a/tests/auth/auth-mode-switch.spec.ts b/tests/auth/auth-mode-switch.spec.ts new file mode 100644 index 000000000..c3f0d0e9a --- /dev/null +++ b/tests/auth/auth-mode-switch.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '../../playwright'; +import { + closeAllCollections, + createCollection, + createRequest, + createFolder, + openRequest, + readField, + saveRequest, + selectAuthMode, + selectRequestPaneTab, + typeIntoField +} from '../utils/page'; + +test.describe('Auth mode switch preserves saved data', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + + test('Request: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => { + await createCollection(page, 'auth-mode-switch-req', await createTmpDir()); + await createRequest(page, 'request-1', 'auth-mode-switch-req', { url: 'https://example.com/api' }); + await openRequest(page, 'auth-mode-switch-req', 'request-1'); + await selectRequestPaneTab(page, 'Auth'); + + await test.step('Save Bearer with a token', async () => { + await selectAuthMode(page, 'Bearer Token'); + await typeIntoField(page, 'Token', 'saved-bearer-token'); + await saveRequest(page); + }); + + await test.step('Bearer → Basic → Bearer restores the saved token (the bug fix)', async () => { + await selectAuthMode(page, 'Basic Auth'); + await selectAuthMode(page, 'Bearer Token'); + + await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token'); + }); + + await test.step('Switching to a non-saved mode shows empty fields (no regression)', async () => { + await selectAuthMode(page, 'Basic Auth'); + + await expect.poll(() => readField(page, 'Username')).toBe(''); + await expect.poll(() => readField(page, 'Password')).toBe(''); + }); + + await test.step('Switching to a third unrelated mode also leaves fields empty', async () => { + // Bearer is the saved mode; Digest has never been touched. + await selectAuthMode(page, 'Digest Auth'); + + await expect.poll(() => readField(page, 'Username')).toBe(''); + await expect.poll(() => readField(page, 'Password')).toBe(''); + }); + + await test.step('Returning once more to Bearer still restores the saved token', async () => { + await selectAuthMode(page, 'Bearer Token'); + await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token'); + }); + }); + + test('Collection: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => { + await createCollection(page, 'auth-mode-switch-col', await createTmpDir()); + + // The collection settings tab opens automatically on creation. + await page.locator('.tab.auth').click(); + + await test.step('Save Bearer at the collection level', async () => { + await selectAuthMode(page, 'Bearer Token'); + await typeIntoField(page, 'Token', 'collection-bearer-token'); + await page.getByRole('button', { name: 'Save' }).click(); + }); + + await test.step('Bearer → Basic → Bearer restores the saved collection token', async () => { + await selectAuthMode(page, 'Basic Auth'); + await selectAuthMode(page, 'Bearer Token'); + + await expect.poll(() => readField(page, 'Token')).toBe('collection-bearer-token'); + }); + }); + + test('Folder: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => { + await createCollection(page, 'auth-mode-switch-folder', await createTmpDir()); + await createFolder(page, 'folder-1', 'auth-mode-switch-folder', true); + + // Open the folder settings tab. + await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).dblclick(); + await page.locator('.tab.auth').click(); + + await test.step('Save Bearer at the folder level', async () => { + await selectAuthMode(page, 'Bearer Token'); + await typeIntoField(page, 'Token', 'folder-bearer-token'); + await page.getByRole('button', { name: 'Save' }).click(); + }); + + await test.step('Bearer → Basic → Bearer restores the saved folder token', async () => { + await selectAuthMode(page, 'Basic Auth'); + await selectAuthMode(page, 'Bearer Token'); + + await expect.poll(() => readField(page, 'Token')).toBe('folder-bearer-token'); + }); + }); +}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 873bd09b3..316d27f5d 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -80,7 +80,12 @@ const openCollection = async (page, collectionName: string) => { * * @returns void */ -const createCollection = async (page, collectionName: string, collectionLocation: string) => { +const createCollection = async ( + page, + collectionName: string, + collectionLocation: string, + format?: 'bru' | 'yml' +) => { await test.step(`Create collection "${collectionName}"`, async () => { await page.getByTestId('collections-header-add-menu').click(); await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click(); @@ -123,6 +128,15 @@ const createCollection = async (page, collectionName: string, collectionLocation await nameInput.fill(collectionName); // Verify the name is correct before creating await expect(nameInput).toHaveValue(collectionName, { timeout: 2000 }); + + if (format) { + await createCollectionModal.locator('.advanced-options .btn-advanced').click(); + await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Show File Format' }).click(); + const formatSelect = createCollectionModal.locator('#format'); + await formatSelect.waitFor({ state: 'visible', timeout: 5000 }); + await formatSelect.selectOption(format); + } + await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click(); // The modal closes via `onClose()` in the form's `onSubmit` success path, @@ -1340,6 +1354,45 @@ const sendAndWaitForResponse = async (page: Page) => { }); }; +const fieldEditor = (page: Page, labelText: string) => + page + .locator('label') + .filter({ hasText: new RegExp(`^${escapeRegExp(labelText)}$`) }) + .locator('..') + .locator('.single-line-editor-wrapper .CodeMirror'); + +/** + * Open the auth mode dropdown and pick a mode by its visible label. + * @param page - The page object + * @param modeLabel - Dropdown item text (e.g. 'Bearer Token', 'Basic Auth') + */ +const selectAuthMode = async (page: Page, modeLabel: string) => { + await page.locator('.auth-mode-label').click(); + await page.locator('.dropdown-item').filter({ hasText: modeLabel }).click(); +}; + +/** + * Type into a single-line CodeMirror editor identified by its sibling label. + * @param page - The page object + * @param labelText - Exact label text next to the editor + * @param value - The text to type + */ +const typeIntoField = async (page: Page, labelText: string, value: string) => { + await fieldEditor(page, labelText).click(); + await page.keyboard.type(value); +}; + +/** + * Read the current value of a single-line CodeMirror editor identified by its sibling label. + * @param page - The page object + * @param labelText - Exact label text next to the editor + */ +const readField = async (page: Page, labelText: string): Promise => { + const editor = fieldEditor(page, labelText).first(); + await editor.waitFor({ state: 'visible' }); + return editor.evaluate((el: any) => (el as any).CodeMirror?.getValue() ?? ''); +}; + const createExampleFromSidebar = async (page: Page, requestName: string, exampleName: string, description: string = '') => { const requestRow = page.locator('.collection-item-name').filter({ hasText: requestName }).first(); @@ -1422,6 +1475,9 @@ export { addTestScript, sendAndWaitForErrorCard, sendAndWaitForResponse, + selectAuthMode, + typeIntoField, + readField, createExampleFromSidebar, openExampleFromSidebar };