mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
* feat: add OAuth 1.0 authentication support Add full OAuth 1.0 (RFC 5849) authentication with support for HMAC-SHA1/256/512, RSA-SHA1/256/512, and PLAINTEXT signature methods. Includes UI components, bru/yml serialization, Postman import, code generation, CLI support, and comprehensive playwright and unit tests. * test: replace real-looking PEM literals with fake markers in oauth1 tests Avoid tripping secret scanners by using obviously fake BEGIN/END markers and non-sensitive base64 content in serialization and round-trip tests. * fix: remove invalid OAuth1 placeholder header from code generator OAuth1 requires runtime-computed nonce, timestamp, and signature that cannot be pre-computed for a static code snippet. Return an empty array instead of emitting an Authorization header with literal <signature>, <timestamp>, <nonce> placeholders. * fix: remove unreachable oauth1 case from WSAuth component The oauth1 switch branch was dead code since it was not in supportedAuthModes and the useEffect would reset it to 'none' before it could render. * fix: remove unused collectionPath param and use path.basename for filename extraction * refactor: rename OAuth1 fields for clarity - tokenSecret → accessTokenSecret - signatureMethod → signatureEncoding - addParamsTo value 'queryparams' → 'query' * refactor: rename addParamsTo to placement in OAuth1 auth * fix: add missing oauth1: null in buildOAuth2Config and upgrade @opencollection/types to 0.9.0 * test: add oauth1 import tests and fix missing oauth1: null in auth assertions * ci: add auth playwright tests workflow for Linux, macOS, and Windows * refactor: rename signatureEncoding to signatureMethod and fix timeline race condition - Rename OAuth1 signatureEncoding to signatureMethod across all packages - Fix timeline showing "No Headers/Body found" when request-sent IPC event arrives after response by retroactively updating the timeline entry - Store requestUid in timeline entries for precise matching - Correct timeline entry timestamp on retroactive update for proper sort order * ci: add OAuth1 CLI tests and reorganize auth actions under oauth1/ - Add CLI tests that run full BRU and YML collections via bru run - Add start-test-server actions for Linux, macOS, and Windows - Move auth e2e and setup actions under auth/oauth1/ directory - Fix Windows Playwright failures caused by unescaped backslashes in collectionPath template variable * ci: reorder auth tests to run E2E tests before CLI tests * ci: start test server after E2E tests to fix port 8081 conflict
222 lines
8.6 KiB
TypeScript
222 lines
8.6 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { test, expect } from '../../../playwright';
|
|
import {
|
|
sendRequestAndWaitForResponse, closeAllCollections, selectEnvironment,
|
|
openCollection, openRequest, selectResponsePaneTab
|
|
} from '../../utils/page';
|
|
import { runCollection, validateRunnerResults } from '../../utils/page/runner';
|
|
|
|
// The test PEM file is gitignored (*.pem). Write it to both fixture directories
|
|
// at module load time so collectionFixturePath includes it when copying.
|
|
|
|
const { TEST_RSA_PRIVATE_KEY } = require('../../../packages/bruno-tests/src/auth/oauth1');
|
|
|
|
const fixtureBase = path.join(__dirname, 'fixtures', 'collections');
|
|
for (const subdir of ['bru', 'yml']) {
|
|
const pemPath = path.join(fixtureBase, subdir, 'test-private-key.pem');
|
|
if (!fs.existsSync(pemPath)) {
|
|
fs.writeFileSync(pemPath, TEST_RSA_PRIVATE_KEY);
|
|
}
|
|
}
|
|
|
|
const BRU_COLLECTION = 'oauth1-testbench-bru';
|
|
const YML_COLLECTION = 'oauth1-testbench-yml';
|
|
|
|
const requests = [
|
|
{ name: 'OAuth1 HMAC-SHA1 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA1 401', status: 401 },
|
|
{ name: 'OAuth1 HMAC-SHA1 POST 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA1 Query Params 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA256 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA256 401', status: 401 },
|
|
{ name: 'OAuth1 HMAC-SHA512 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA512 401', status: 401 },
|
|
{ name: 'OAuth1 PLAINTEXT 200', status: 200 },
|
|
{ name: 'OAuth1 PLAINTEXT 401', status: 401 },
|
|
{ name: 'OAuth1 PLAINTEXT Query Params 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 Query Params 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA256 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA512 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 Variable Key 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 File Key 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA1 Body 200', status: 200 },
|
|
{ name: 'OAuth1 PLAINTEXT Body 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA256 Body 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 Body 200', status: 200 },
|
|
{ name: 'OAuth1 RSA-SHA1 Body formurlencoded 200', status: 200 },
|
|
{ name: 'OAuth1 HMAC-SHA1 Body JSON 200', status: 200 }
|
|
];
|
|
|
|
const sendAllRequests = async (page, collectionName: string) => {
|
|
await openCollection(page, collectionName);
|
|
await selectEnvironment(page, 'Local', 'collection');
|
|
|
|
for (const { name, status } of requests) {
|
|
await test.step(name, async () => {
|
|
await openRequest(page, collectionName, name);
|
|
await sendRequestAndWaitForResponse(page, status);
|
|
});
|
|
}
|
|
};
|
|
|
|
const runAndValidate = async (page, collectionName: string) => {
|
|
await runCollection(page, collectionName);
|
|
await validateRunnerResults(page, {
|
|
totalRequests: requests.length,
|
|
passed: requests.length,
|
|
failed: 0
|
|
});
|
|
};
|
|
|
|
/**
|
|
* After sending a request, switch to the Timeline tab, expand the latest timeline item,
|
|
* and return locators for the request URL and headers section.
|
|
*/
|
|
const openTimelineRequest = async (page) => {
|
|
await selectResponsePaneTab(page, 'Timeline');
|
|
|
|
// Click the first (latest) timeline item header to expand it
|
|
const timelineItem = page.locator('.timeline-item').first();
|
|
await timelineItem.locator('.oauth-request-item-header').click();
|
|
|
|
return timelineItem;
|
|
};
|
|
|
|
const verifyPlacement = async (page, collectionName: string, requestName: string, placement: 'header' | 'query' | 'body') => {
|
|
await openRequest(page, collectionName, requestName);
|
|
await sendRequestAndWaitForResponse(page, 200);
|
|
|
|
const timelineItem = await openTimelineRequest(page);
|
|
const content = timelineItem.locator('.timeline-item-content');
|
|
|
|
if (placement === 'header') {
|
|
await expect(content).toContainText('Authorization');
|
|
await expect(content).toContainText('OAuth');
|
|
} else if (placement === 'query') {
|
|
const urlPre = content.locator('pre').first();
|
|
await expect(urlPre).toContainText('oauth_consumer_key');
|
|
} else {
|
|
// Body: oauth params should be in the request body, not in URL or Authorization header
|
|
const urlPre = content.locator('pre').first();
|
|
await expect(urlPre).not.toContainText('oauth_consumer_key');
|
|
// Body section is expanded by default — verify oauth params are in the body
|
|
await expect(content.locator('.collapsible-section').filter({ hasText: 'Body' })).toContainText('oauth_consumer_key');
|
|
}
|
|
};
|
|
|
|
test.describe('OAuth 1.0 Runner', () => {
|
|
test.afterAll(async ({ pageWithUserData: page }) => {
|
|
await closeAllCollections(page);
|
|
});
|
|
|
|
test.describe('[bru]', () => {
|
|
test('Send individual requests', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await sendAllRequests(page, BRU_COLLECTION);
|
|
});
|
|
|
|
test('Run collection and verify all assertions pass', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await runAndValidate(page, BRU_COLLECTION);
|
|
});
|
|
|
|
test('Verify Add Params To placement via timeline', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await openCollection(page, BRU_COLLECTION);
|
|
await selectEnvironment(page, 'Local', 'collection');
|
|
|
|
await test.step('Header: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 200', 'header');
|
|
});
|
|
|
|
await test.step('Query Params: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Query Params: PLAINTEXT', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 PLAINTEXT Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Query Params: RSA-SHA1', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 RSA-SHA1 Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: PLAINTEXT', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 PLAINTEXT Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA256', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA256 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: RSA-SHA1', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 RSA-SHA1 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA1 JSON (non-form body)', async () => {
|
|
await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Body JSON 200', 'body');
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('[yml]', () => {
|
|
test('Send individual requests', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await sendAllRequests(page, YML_COLLECTION);
|
|
});
|
|
|
|
test('Run collection and verify all assertions pass', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await runAndValidate(page, YML_COLLECTION);
|
|
});
|
|
|
|
test('Verify Add Params To placement via timeline', async ({ pageWithUserData: page }) => {
|
|
test.setTimeout(3 * 60 * 1000);
|
|
await openCollection(page, YML_COLLECTION);
|
|
await selectEnvironment(page, 'Local', 'collection');
|
|
|
|
await test.step('Header: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 200', 'header');
|
|
});
|
|
|
|
await test.step('Query Params: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Query Params: PLAINTEXT', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 PLAINTEXT Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Query Params: RSA-SHA1', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 RSA-SHA1 Query Params 200', 'query');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA1', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: PLAINTEXT', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 PLAINTEXT Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA256', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA256 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: RSA-SHA1', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 RSA-SHA1 Body 200', 'body');
|
|
});
|
|
|
|
await test.step('Body: HMAC-SHA1 JSON (non-form body)', async () => {
|
|
await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Body JSON 200', 'body');
|
|
});
|
|
});
|
|
});
|
|
});
|