Skip to content

Commit 55774a8

Browse files
authored
fix: restore saved credentials when switching back to original auth mode (usebruno#7911)
1 parent 736c050 commit 55774a8

4 files changed

Lines changed: 248 additions & 7 deletions

File tree

packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,8 +1683,14 @@ export const collectionsSlice = createSlice({
16831683
if (!item.draft) {
16841684
item.draft = cloneDeep(item);
16851685
}
1686-
item.draft.request.auth = {};
1687-
item.draft.request.auth.mode = action.payload.mode;
1686+
const newMode = action.payload.mode;
1687+
const savedAuth = get(item, 'request.auth');
1688+
const savedMode = get(savedAuth, 'mode');
1689+
if (newMode === savedMode) {
1690+
item.draft.request.auth = cloneDeep(savedAuth);
1691+
} else {
1692+
item.draft.request.auth = { mode: newMode };
1693+
}
16881694
}
16891695
}
16901696
},
@@ -2113,8 +2119,14 @@ export const collectionsSlice = createSlice({
21132119
root: cloneDeep(collection.root)
21142120
};
21152121
}
2116-
set(collection, 'draft.root.request.auth', {});
2117-
set(collection, 'draft.root.request.auth.mode', action.payload.mode);
2122+
const newMode = action.payload.mode;
2123+
const savedAuth = get(collection, 'root.request.auth');
2124+
const savedMode = get(savedAuth, 'mode');
2125+
if (newMode === savedMode) {
2126+
set(collection, 'draft.root.request.auth', cloneDeep(savedAuth));
2127+
} else {
2128+
set(collection, 'draft.root.request.auth', { mode: newMode });
2129+
}
21182130
}
21192131
},
21202132
updateCollectionAuth: (state, action) => {
@@ -3322,8 +3334,14 @@ export const collectionsSlice = createSlice({
33223334
if (!folder.draft) {
33233335
folder.draft = cloneDeep(folder.root);
33243336
}
3325-
set(folder, 'draft.request.auth', {});
3326-
set(folder, 'draft.request.auth.mode', action.payload.mode);
3337+
const newMode = action.payload.mode;
3338+
const savedAuth = get(folder, 'root.request.auth');
3339+
const savedMode = get(savedAuth, 'mode');
3340+
if (newMode === savedMode) {
3341+
set(folder, 'draft.request.auth', cloneDeep(savedAuth));
3342+
} else {
3343+
set(folder, 'draft.request.auth', { mode: newMode });
3344+
}
33273345
}
33283346
},
33293347
streamDataReceived: (state, action) => {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { test, expect } from '../../playwright';
2+
import {
3+
closeAllCollections,
4+
createCollection,
5+
createRequest,
6+
openRequest,
7+
readField,
8+
saveRequest,
9+
selectAuthMode,
10+
selectRequestPaneTab,
11+
typeIntoField
12+
} from '../utils/page';
13+
14+
type CollectionFormat = 'bru' | 'yml';
15+
16+
const runDraftIndicatorScenario = (format: CollectionFormat) => {
17+
test(`(${format}) switching back to the saved auth mode hides the draft indicator`, async ({ page, createTmpDir }) => {
18+
const collectionName = `auth-draft-indicator-${format}`;
19+
const requestName = `request-${format}`;
20+
21+
await createCollection(page, collectionName, await createTmpDir(), format);
22+
await createRequest(page, requestName, collectionName, { url: 'https://example.com/api' });
23+
await openRequest(page, collectionName, requestName);
24+
await selectRequestPaneTab(page, 'Auth');
25+
26+
const requestTab = page
27+
.locator('.request-tab')
28+
.filter({ has: page.locator('.tab-label', { hasText: requestName }) });
29+
30+
await test.step('Save Bearer with a token — draft indicator clears', async () => {
31+
await selectAuthMode(page, 'Bearer Token');
32+
await typeIntoField(page, 'Token', 'saved-bearer-token');
33+
await saveRequest(page);
34+
35+
await expect(requestTab.locator('.close-icon')).toBeVisible();
36+
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
37+
});
38+
39+
await test.step('Switching to Basic Auth without saving shows the draft indicator', async () => {
40+
await selectAuthMode(page, 'Basic Auth');
41+
42+
await expect(requestTab.locator('.has-changes-icon')).toBeVisible();
43+
await expect(requestTab.locator('.close-icon')).not.toBeVisible();
44+
});
45+
46+
await test.step('Switching back to the saved Bearer mode without saving hides the draft indicator', async () => {
47+
await selectAuthMode(page, 'Bearer Token');
48+
49+
// Saved token is restored
50+
await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token');
51+
52+
// Draft now deep-equals the saved state — indicator must be gone
53+
await expect(requestTab.locator('.close-icon')).toBeVisible();
54+
await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();
55+
});
56+
});
57+
};
58+
59+
test.describe('Auth mode switch — draft indicator clears on return to saved mode', () => {
60+
test.afterEach(async ({ page }) => {
61+
await closeAllCollections(page);
62+
});
63+
64+
runDraftIndicatorScenario('bru');
65+
runDraftIndicatorScenario('yml');
66+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { test, expect } from '../../playwright';
2+
import {
3+
closeAllCollections,
4+
createCollection,
5+
createRequest,
6+
createFolder,
7+
openRequest,
8+
readField,
9+
saveRequest,
10+
selectAuthMode,
11+
selectRequestPaneTab,
12+
typeIntoField
13+
} from '../utils/page';
14+
15+
test.describe('Auth mode switch preserves saved data', () => {
16+
test.afterEach(async ({ page }) => {
17+
await closeAllCollections(page);
18+
});
19+
20+
test('Request: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => {
21+
await createCollection(page, 'auth-mode-switch-req', await createTmpDir());
22+
await createRequest(page, 'request-1', 'auth-mode-switch-req', { url: 'https://example.com/api' });
23+
await openRequest(page, 'auth-mode-switch-req', 'request-1');
24+
await selectRequestPaneTab(page, 'Auth');
25+
26+
await test.step('Save Bearer with a token', async () => {
27+
await selectAuthMode(page, 'Bearer Token');
28+
await typeIntoField(page, 'Token', 'saved-bearer-token');
29+
await saveRequest(page);
30+
});
31+
32+
await test.step('Bearer → Basic → Bearer restores the saved token (the bug fix)', async () => {
33+
await selectAuthMode(page, 'Basic Auth');
34+
await selectAuthMode(page, 'Bearer Token');
35+
36+
await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token');
37+
});
38+
39+
await test.step('Switching to a non-saved mode shows empty fields (no regression)', async () => {
40+
await selectAuthMode(page, 'Basic Auth');
41+
42+
await expect.poll(() => readField(page, 'Username')).toBe('');
43+
await expect.poll(() => readField(page, 'Password')).toBe('');
44+
});
45+
46+
await test.step('Switching to a third unrelated mode also leaves fields empty', async () => {
47+
// Bearer is the saved mode; Digest has never been touched.
48+
await selectAuthMode(page, 'Digest Auth');
49+
50+
await expect.poll(() => readField(page, 'Username')).toBe('');
51+
await expect.poll(() => readField(page, 'Password')).toBe('');
52+
});
53+
54+
await test.step('Returning once more to Bearer still restores the saved token', async () => {
55+
await selectAuthMode(page, 'Bearer Token');
56+
await expect.poll(() => readField(page, 'Token')).toBe('saved-bearer-token');
57+
});
58+
});
59+
60+
test('Collection: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => {
61+
await createCollection(page, 'auth-mode-switch-col', await createTmpDir());
62+
63+
// The collection settings tab opens automatically on creation.
64+
await page.locator('.tab.auth').click();
65+
66+
await test.step('Save Bearer at the collection level', async () => {
67+
await selectAuthMode(page, 'Bearer Token');
68+
await typeIntoField(page, 'Token', 'collection-bearer-token');
69+
await page.getByRole('button', { name: 'Save' }).click();
70+
});
71+
72+
await test.step('Bearer → Basic → Bearer restores the saved collection token', async () => {
73+
await selectAuthMode(page, 'Basic Auth');
74+
await selectAuthMode(page, 'Bearer Token');
75+
76+
await expect.poll(() => readField(page, 'Token')).toBe('collection-bearer-token');
77+
});
78+
});
79+
80+
test('Folder: switching back to the saved mode restores its credentials', async ({ page, createTmpDir }) => {
81+
await createCollection(page, 'auth-mode-switch-folder', await createTmpDir());
82+
await createFolder(page, 'folder-1', 'auth-mode-switch-folder', true);
83+
84+
// Open the folder settings tab.
85+
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).dblclick();
86+
await page.locator('.tab.auth').click();
87+
88+
await test.step('Save Bearer at the folder level', async () => {
89+
await selectAuthMode(page, 'Bearer Token');
90+
await typeIntoField(page, 'Token', 'folder-bearer-token');
91+
await page.getByRole('button', { name: 'Save' }).click();
92+
});
93+
94+
await test.step('Bearer → Basic → Bearer restores the saved folder token', async () => {
95+
await selectAuthMode(page, 'Basic Auth');
96+
await selectAuthMode(page, 'Bearer Token');
97+
98+
await expect.poll(() => readField(page, 'Token')).toBe('folder-bearer-token');
99+
});
100+
});
101+
});

tests/utils/page/actions.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ const openCollection = async (page, collectionName: string) => {
8080
*
8181
* @returns void
8282
*/
83-
const createCollection = async (page, collectionName: string, collectionLocation: string) => {
83+
const createCollection = async (
84+
page,
85+
collectionName: string,
86+
collectionLocation: string,
87+
format?: 'bru' | 'yml'
88+
) => {
8489
await test.step(`Create collection "${collectionName}"`, async () => {
8590
await page.getByTestId('collections-header-add-menu').click();
8691
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();
@@ -123,6 +128,15 @@ const createCollection = async (page, collectionName: string, collectionLocation
123128
await nameInput.fill(collectionName);
124129
// Verify the name is correct before creating
125130
await expect(nameInput).toHaveValue(collectionName, { timeout: 2000 });
131+
132+
if (format) {
133+
await createCollectionModal.locator('.advanced-options .btn-advanced').click();
134+
await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Show File Format' }).click();
135+
const formatSelect = createCollectionModal.locator('#format');
136+
await formatSelect.waitFor({ state: 'visible', timeout: 5000 });
137+
await formatSelect.selectOption(format);
138+
}
139+
126140
await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click();
127141

128142
// The modal closes via `onClose()` in the form's `onSubmit` success path,
@@ -1340,6 +1354,45 @@ const sendAndWaitForResponse = async (page: Page) => {
13401354
});
13411355
};
13421356

1357+
const fieldEditor = (page: Page, labelText: string) =>
1358+
page
1359+
.locator('label')
1360+
.filter({ hasText: new RegExp(`^${escapeRegExp(labelText)}$`) })
1361+
.locator('..')
1362+
.locator('.single-line-editor-wrapper .CodeMirror');
1363+
1364+
/**
1365+
* Open the auth mode dropdown and pick a mode by its visible label.
1366+
* @param page - The page object
1367+
* @param modeLabel - Dropdown item text (e.g. 'Bearer Token', 'Basic Auth')
1368+
*/
1369+
const selectAuthMode = async (page: Page, modeLabel: string) => {
1370+
await page.locator('.auth-mode-label').click();
1371+
await page.locator('.dropdown-item').filter({ hasText: modeLabel }).click();
1372+
};
1373+
1374+
/**
1375+
* Type into a single-line CodeMirror editor identified by its sibling label.
1376+
* @param page - The page object
1377+
* @param labelText - Exact label text next to the editor
1378+
* @param value - The text to type
1379+
*/
1380+
const typeIntoField = async (page: Page, labelText: string, value: string) => {
1381+
await fieldEditor(page, labelText).click();
1382+
await page.keyboard.type(value);
1383+
};
1384+
1385+
/**
1386+
* Read the current value of a single-line CodeMirror editor identified by its sibling label.
1387+
* @param page - The page object
1388+
* @param labelText - Exact label text next to the editor
1389+
*/
1390+
const readField = async (page: Page, labelText: string): Promise<string> => {
1391+
const editor = fieldEditor(page, labelText).first();
1392+
await editor.waitFor({ state: 'visible' });
1393+
return editor.evaluate((el: any) => (el as any).CodeMirror?.getValue() ?? '');
1394+
};
1395+
13431396
const createExampleFromSidebar = async (page: Page, requestName: string, exampleName: string, description: string = '') => {
13441397
const requestRow = page.locator('.collection-item-name').filter({ hasText: requestName }).first();
13451398

@@ -1422,6 +1475,9 @@ export {
14221475
addTestScript,
14231476
sendAndWaitForErrorCard,
14241477
sendAndWaitForResponse,
1478+
selectAuthMode,
1479+
typeIntoField,
1480+
readField,
14251481
createExampleFromSidebar,
14261482
openExampleFromSidebar
14271483
};

0 commit comments

Comments
 (0)