mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-22 20:25:38 +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
851 lines
22 KiB
JavaScript
851 lines
22 KiB
JavaScript
const bruToJson = require('../src/bruToJson');
|
||
const jsonToBru = require('../src/jsonToBru');
|
||
const collectionBruToJson = require('../src/collectionBruToJson');
|
||
const jsonToCollectionBru = require('../src/jsonToCollectionBru');
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// bruToJson – request-level parsing
|
||
// ---------------------------------------------------------------------------
|
||
describe('OAuth1 bruToJson (request-level)', () => {
|
||
it('should parse all oauth1 fields with text private key', () => {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 Test
|
||
type: http
|
||
seq: 1
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
body: none
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: my_consumer_key
|
||
consumer_secret: my_consumer_secret
|
||
access_token: my_access_token
|
||
token_secret: my_token_secret
|
||
callback_url: https://example.com/callback
|
||
verifier: my_verifier
|
||
signature_method: HMAC-SHA1
|
||
private_key: my_private_key
|
||
timestamp: 1234567890
|
||
nonce: abc123
|
||
version: 1.0
|
||
realm: my_realm
|
||
placement: header
|
||
include_body_hash: true
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
|
||
expect(result.auth.oauth1).toEqual({
|
||
consumerKey: 'my_consumer_key',
|
||
consumerSecret: 'my_consumer_secret',
|
||
accessToken: 'my_access_token',
|
||
accessTokenSecret: 'my_token_secret',
|
||
callbackUrl: 'https://example.com/callback',
|
||
verifier: 'my_verifier',
|
||
signatureMethod: 'HMAC-SHA1',
|
||
privateKey: 'my_private_key',
|
||
privateKeyType: 'text',
|
||
timestamp: '1234567890',
|
||
nonce: 'abc123',
|
||
version: '1.0',
|
||
realm: 'my_realm',
|
||
placement: 'header',
|
||
includeBodyHash: true
|
||
});
|
||
});
|
||
|
||
it('should parse empty/missing optional fields as empty strings', () => {
|
||
const input = `
|
||
meta {
|
||
name: Minimal OAuth1
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret:
|
||
access_token:
|
||
token_secret:
|
||
callback_url:
|
||
verifier:
|
||
signature_method: HMAC-SHA1
|
||
private_key:
|
||
timestamp:
|
||
nonce:
|
||
version:
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
|
||
expect(result.auth.oauth1.consumerKey).toBe('ck');
|
||
expect(result.auth.oauth1.consumerSecret).toBe('');
|
||
expect(result.auth.oauth1.accessToken).toBe('');
|
||
expect(result.auth.oauth1.accessTokenSecret).toBe('');
|
||
expect(result.auth.oauth1.callbackUrl).toBe('');
|
||
expect(result.auth.oauth1.verifier).toBe('');
|
||
expect(result.auth.oauth1.privateKey).toBe('');
|
||
expect(result.auth.oauth1.privateKeyType).toBe('text');
|
||
expect(result.auth.oauth1.timestamp).toBe('');
|
||
expect(result.auth.oauth1.nonce).toBe('');
|
||
expect(result.auth.oauth1.version).toBe('');
|
||
expect(result.auth.oauth1.realm).toBe('');
|
||
expect(result.auth.oauth1.includeBodyHash).toBe(false);
|
||
});
|
||
|
||
it('should parse @file() private key as file type', () => {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 File Key
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret:
|
||
access_token: at
|
||
token_secret: ts
|
||
callback_url:
|
||
verifier:
|
||
signature_method: RSA-SHA1
|
||
private_key: @file(keys/my-private-key.pem)
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
|
||
expect(result.auth.oauth1.privateKey).toBe('keys/my-private-key.pem');
|
||
expect(result.auth.oauth1.privateKeyType).toBe('file');
|
||
});
|
||
|
||
it('should parse multiline private key (triple-quoted PEM)', () => {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 Multiline PEM
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret: cs
|
||
access_token: at
|
||
token_secret: ts
|
||
callback_url:
|
||
verifier:
|
||
signature_method: RSA-SHA1
|
||
private_key: '''
|
||
-----BEGIN FAKE TEST KEY-----
|
||
TESTREPLACEMENTdGhpcyBpcyBub3QgYQ==
|
||
-----END FAKE TEST KEY-----
|
||
'''
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
|
||
expect(result.auth.oauth1.privateKeyType).toBe('text');
|
||
expect(result.auth.oauth1.privateKey).toContain('-----BEGIN FAKE TEST KEY-----');
|
||
expect(result.auth.oauth1.privateKey).toContain('-----END FAKE TEST KEY-----');
|
||
expect(result.auth.oauth1.privateKey).toContain('TESTREPLACEMENTdGhpcyBpcyBub3QgYQ==');
|
||
// Verify no leading spaces are preserved in the parsed key lines
|
||
const keyLines = result.auth.oauth1.privateKey.split('\n').filter((l) => l.length > 0);
|
||
keyLines.forEach((line) => {
|
||
expect(line).not.toMatch(/^\s/);
|
||
});
|
||
});
|
||
|
||
it('should parse variable reference in private key as text type', () => {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 Variable Key
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret:
|
||
access_token: at
|
||
token_secret: ts
|
||
callback_url:
|
||
verifier:
|
||
signature_method: RSA-SHA1
|
||
private_key: {{my_private_key}}
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
|
||
expect(result.auth.oauth1.privateKey).toBe('{{my_private_key}}');
|
||
expect(result.auth.oauth1.privateKeyType).toBe('text');
|
||
});
|
||
|
||
it('should parse all signature methods correctly', () => {
|
||
const signatureMethods = ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT'];
|
||
|
||
for (const method of signatureMethods) {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 ${method}
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret: cs
|
||
access_token: at
|
||
token_secret: ts
|
||
callback_url:
|
||
verifier:
|
||
signature_method: ${method}
|
||
private_key:
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
expect(result.auth.oauth1.signatureMethod).toBe(method);
|
||
}
|
||
});
|
||
|
||
it('should parse placement values: header, query, body', () => {
|
||
for (const placement of ['header', 'query', 'body']) {
|
||
const input = `
|
||
meta {
|
||
name: OAuth1 Params To ${placement}
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret: cs
|
||
access_token: at
|
||
token_secret: ts
|
||
callback_url:
|
||
verifier:
|
||
signature_method: HMAC-SHA1
|
||
private_key:
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: ${placement}
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = bruToJson(input);
|
||
expect(result.auth.oauth1.placement).toBe(placement);
|
||
}
|
||
});
|
||
|
||
it('should parse include_body_hash true and false', () => {
|
||
const makeInput = (val) => `
|
||
meta {
|
||
name: OAuth1 Body Hash
|
||
type: http
|
||
}
|
||
|
||
get {
|
||
url: https://api.example.com/resource
|
||
auth: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret: cs
|
||
access_token:
|
||
token_secret:
|
||
callback_url:
|
||
verifier:
|
||
signature_method: HMAC-SHA1
|
||
private_key:
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: ${val}
|
||
}
|
||
`.trim();
|
||
|
||
expect(bruToJson(makeInput('true')).auth.oauth1.includeBodyHash).toBe(true);
|
||
expect(bruToJson(makeInput('false')).auth.oauth1.includeBodyHash).toBe(false);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// collectionBruToJson – collection/folder-level parsing
|
||
// ---------------------------------------------------------------------------
|
||
describe('OAuth1 collectionBruToJson (collection/folder-level)', () => {
|
||
it('should parse all oauth1 fields at collection level', () => {
|
||
const input = `
|
||
auth {
|
||
mode: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: col_consumer_key
|
||
consumer_secret: col_consumer_secret
|
||
access_token: col_access_token
|
||
token_secret: col_token_secret
|
||
callback_url: https://col.example.com/cb
|
||
verifier: col_verifier
|
||
signature_method: HMAC-SHA256
|
||
private_key: col_private_key
|
||
timestamp: 9999999999
|
||
nonce: col_nonce
|
||
version: 1.0
|
||
realm: col_realm
|
||
placement: query
|
||
include_body_hash: true
|
||
}
|
||
`.trim();
|
||
|
||
const result = collectionBruToJson(input);
|
||
|
||
expect(result.auth.mode).toBe('oauth1');
|
||
expect(result.auth.oauth1).toEqual({
|
||
consumerKey: 'col_consumer_key',
|
||
consumerSecret: 'col_consumer_secret',
|
||
accessToken: 'col_access_token',
|
||
accessTokenSecret: 'col_token_secret',
|
||
callbackUrl: 'https://col.example.com/cb',
|
||
verifier: 'col_verifier',
|
||
signatureMethod: 'HMAC-SHA256',
|
||
privateKey: 'col_private_key',
|
||
privateKeyType: 'text',
|
||
timestamp: '9999999999',
|
||
nonce: 'col_nonce',
|
||
version: '1.0',
|
||
realm: 'col_realm',
|
||
placement: 'query',
|
||
includeBodyHash: true
|
||
});
|
||
});
|
||
|
||
it('should parse @file() private key at collection level', () => {
|
||
const input = `
|
||
auth {
|
||
mode: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret:
|
||
access_token:
|
||
token_secret:
|
||
callback_url:
|
||
verifier:
|
||
signature_method: RSA-SHA1
|
||
private_key: @file(certs/private.pem)
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = collectionBruToJson(input);
|
||
|
||
expect(result.auth.oauth1.privateKey).toBe('certs/private.pem');
|
||
expect(result.auth.oauth1.privateKeyType).toBe('file');
|
||
});
|
||
|
||
it('should parse multiline private key at collection level', () => {
|
||
const input = `
|
||
auth {
|
||
mode: oauth1
|
||
}
|
||
|
||
auth:oauth1 {
|
||
consumer_key: ck
|
||
consumer_secret:
|
||
access_token:
|
||
token_secret:
|
||
callback_url:
|
||
verifier:
|
||
signature_method: RSA-SHA256
|
||
private_key: '''
|
||
-----BEGIN FAKE RSA TEST KEY-----
|
||
RkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz
|
||
-----END FAKE RSA TEST KEY-----
|
||
'''
|
||
timestamp:
|
||
nonce:
|
||
version: 1.0
|
||
realm:
|
||
placement: header
|
||
include_body_hash: false
|
||
}
|
||
`.trim();
|
||
|
||
const result = collectionBruToJson(input);
|
||
|
||
expect(result.auth.oauth1.privateKeyType).toBe('text');
|
||
expect(result.auth.oauth1.privateKey).toContain('-----BEGIN FAKE RSA TEST KEY-----');
|
||
expect(result.auth.oauth1.privateKey).toContain('RkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz');
|
||
expect(result.auth.oauth1.privateKey).toContain('-----END FAKE RSA TEST KEY-----');
|
||
// Verify no leading spaces are preserved in the parsed key lines
|
||
const keyLines = result.auth.oauth1.privateKey.split('\n').filter((l) => l.length > 0);
|
||
keyLines.forEach((line) => {
|
||
expect(line).not.toMatch(/^\s/);
|
||
});
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// jsonToBru – request-level serialization
|
||
// ---------------------------------------------------------------------------
|
||
describe('OAuth1 jsonToBru (request-level)', () => {
|
||
it('should serialize all oauth1 fields with text private key', () => {
|
||
const json = {
|
||
meta: { name: 'OAuth1 Serialize', type: 'http', seq: 1 },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', body: 'none', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: 'cs',
|
||
accessToken: 'at',
|
||
accessTokenSecret: 'ts',
|
||
callbackUrl: 'https://example.com/cb',
|
||
verifier: 'v',
|
||
signatureMethod: 'HMAC-SHA1',
|
||
privateKey: 'pk',
|
||
privateKeyType: 'text',
|
||
timestamp: '123',
|
||
nonce: 'n',
|
||
version: '1.0',
|
||
realm: 'r',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
|
||
expect(bru).toContain('auth:oauth1 {');
|
||
expect(bru).toContain('consumer_key: ck');
|
||
expect(bru).toContain('consumer_secret: cs');
|
||
expect(bru).toContain('access_token: at');
|
||
expect(bru).toContain('token_secret: ts');
|
||
expect(bru).toContain('callback_url: https://example.com/cb');
|
||
expect(bru).toContain('verifier: v');
|
||
expect(bru).toContain('signature_method: HMAC-SHA1');
|
||
expect(bru).toContain('private_key: pk');
|
||
expect(bru).toContain('timestamp: 123');
|
||
expect(bru).toContain('nonce: n');
|
||
expect(bru).toContain('version: 1.0');
|
||
expect(bru).toContain('realm: r');
|
||
expect(bru).toContain('placement: header');
|
||
expect(bru).toContain('include_body_hash: false');
|
||
});
|
||
|
||
it('should serialize file private key with @file() wrapper', () => {
|
||
const json = {
|
||
meta: { name: 'OAuth1 File', type: 'http', seq: 1 },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA1',
|
||
privateKey: 'keys/private.pem',
|
||
privateKeyType: 'file',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
|
||
expect(bru).toContain('private_key: @file(keys/private.pem)');
|
||
});
|
||
|
||
it('should serialize multiline private key with triple quotes', () => {
|
||
const pem = '-----BEGIN FAKE TEST KEY-----\nTESTREPLACEMENTdGhpcyBpcyBub3QgYQ==\nRkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz\n-----END FAKE TEST KEY-----';
|
||
|
||
const json = {
|
||
meta: { name: 'OAuth1 PEM', type: 'http', seq: 1 },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA1',
|
||
privateKey: pem,
|
||
privateKeyType: 'text',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
|
||
expect(bru).toContain('private_key: \'\'\'');
|
||
expect(bru).toContain('-----BEGIN FAKE TEST KEY-----');
|
||
expect(bru).toContain('-----END FAKE TEST KEY-----');
|
||
});
|
||
|
||
it('should serialize empty optional fields', () => {
|
||
const json = {
|
||
meta: { name: 'OAuth1 Empty', type: 'http', seq: 1 },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'HMAC-SHA1',
|
||
privateKey: '',
|
||
privateKeyType: 'text',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
|
||
// Empty fields should still be present
|
||
expect(bru).toMatch(/consumer_secret:\s*$/m);
|
||
expect(bru).toMatch(/access_token:\s*$/m);
|
||
expect(bru).toMatch(/token_secret:\s*$/m);
|
||
expect(bru).toMatch(/callback_url:\s*$/m);
|
||
expect(bru).toMatch(/verifier:\s*$/m);
|
||
expect(bru).toMatch(/private_key:\s*$/m);
|
||
expect(bru).toMatch(/timestamp:\s*$/m);
|
||
expect(bru).toMatch(/nonce:\s*$/m);
|
||
expect(bru).toMatch(/version:\s*$/m);
|
||
expect(bru).toMatch(/realm:\s*$/m);
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// jsonToCollectionBru – collection/folder-level serialization
|
||
// ---------------------------------------------------------------------------
|
||
describe('OAuth1 jsonToCollectionBru (collection/folder-level)', () => {
|
||
it('should serialize oauth1 at collection level', () => {
|
||
const json = {
|
||
auth: {
|
||
mode: 'oauth1',
|
||
oauth1: {
|
||
consumerKey: 'col_ck',
|
||
consumerSecret: 'col_cs',
|
||
accessToken: 'col_at',
|
||
accessTokenSecret: 'col_ts',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'HMAC-SHA256',
|
||
privateKey: '',
|
||
privateKeyType: 'text',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'query',
|
||
includeBodyHash: true
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToCollectionBru(json);
|
||
|
||
expect(bru).toContain('auth {');
|
||
expect(bru).toContain('mode: oauth1');
|
||
expect(bru).toContain('auth:oauth1 {');
|
||
expect(bru).toContain('consumer_key: col_ck');
|
||
expect(bru).toContain('consumer_secret: col_cs');
|
||
expect(bru).toContain('signature_method: HMAC-SHA256');
|
||
expect(bru).toContain('placement: query');
|
||
expect(bru).toContain('include_body_hash: true');
|
||
});
|
||
|
||
it('should serialize @file() private key at collection level', () => {
|
||
const json = {
|
||
auth: {
|
||
mode: 'oauth1',
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA1',
|
||
privateKey: 'certs/key.pem',
|
||
privateKeyType: 'file',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToCollectionBru(json);
|
||
|
||
expect(bru).toContain('private_key: @file(certs/key.pem)');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Round-trip tests – bruToJson → jsonToBru → bruToJson
|
||
// ---------------------------------------------------------------------------
|
||
describe('OAuth1 round-trip (request-level)', () => {
|
||
it('should survive round-trip with all fields populated', () => {
|
||
const json = {
|
||
meta: { name: 'OAuth1 Roundtrip', type: 'http', seq: '1' },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', body: 'none', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: 'cs',
|
||
accessToken: 'at',
|
||
accessTokenSecret: 'ts',
|
||
callbackUrl: 'https://example.com/cb',
|
||
verifier: 'ver',
|
||
signatureMethod: 'HMAC-SHA1',
|
||
privateKey: 'inline_pk',
|
||
privateKeyType: 'text',
|
||
timestamp: '1234567890',
|
||
nonce: 'abc',
|
||
version: '1.0',
|
||
realm: 'testrealm',
|
||
placement: 'header',
|
||
includeBodyHash: true
|
||
}
|
||
},
|
||
settings: { encodeUrl: true, timeout: 0 }
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
const parsed = bruToJson(bru);
|
||
|
||
expect(parsed.auth.oauth1).toEqual(json.auth.oauth1);
|
||
});
|
||
|
||
it('should survive round-trip with file private key', () => {
|
||
const json = {
|
||
meta: { name: 'OAuth1 File RT', type: 'http', seq: '1' },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: 'at',
|
||
accessTokenSecret: 'ts',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA1',
|
||
privateKey: 'keys/private.pem',
|
||
privateKeyType: 'file',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
},
|
||
settings: { encodeUrl: true, timeout: 0 }
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
const parsed = bruToJson(bru);
|
||
|
||
expect(parsed.auth.oauth1.privateKey).toBe('keys/private.pem');
|
||
expect(parsed.auth.oauth1.privateKeyType).toBe('file');
|
||
});
|
||
|
||
it('should survive round-trip with multiline PEM private key', () => {
|
||
const pem = '-----BEGIN FAKE TEST KEY-----\nTESTREPLACEMENTdGhpcyBpcyBub3QgYQ==\nRkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz\n-----END FAKE TEST KEY-----';
|
||
|
||
const json = {
|
||
meta: { name: 'OAuth1 PEM RT', type: 'http', seq: '1' },
|
||
http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' },
|
||
auth: {
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA256',
|
||
privateKey: pem,
|
||
privateKeyType: 'text',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
},
|
||
settings: { encodeUrl: true, timeout: 0 }
|
||
};
|
||
|
||
const bru = jsonToBru(json);
|
||
const parsed = bruToJson(bru);
|
||
|
||
expect(parsed.auth.oauth1.privateKey).toBe(pem);
|
||
expect(parsed.auth.oauth1.privateKeyType).toBe('text');
|
||
});
|
||
});
|
||
|
||
describe('OAuth1 round-trip (collection-level)', () => {
|
||
it('should survive round-trip at collection level', () => {
|
||
const json = {
|
||
auth: {
|
||
mode: 'oauth1',
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: 'cs',
|
||
accessToken: 'at',
|
||
accessTokenSecret: 'ts',
|
||
callbackUrl: 'https://example.com/cb',
|
||
verifier: 'ver',
|
||
signatureMethod: 'HMAC-SHA512',
|
||
privateKey: '',
|
||
privateKeyType: 'text',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'body',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToCollectionBru(json);
|
||
const parsed = collectionBruToJson(bru);
|
||
|
||
expect(parsed.auth.mode).toBe('oauth1');
|
||
expect(parsed.auth.oauth1).toEqual(json.auth.oauth1);
|
||
});
|
||
|
||
it('should survive round-trip with file key at collection level', () => {
|
||
const json = {
|
||
auth: {
|
||
mode: 'oauth1',
|
||
oauth1: {
|
||
consumerKey: 'ck',
|
||
consumerSecret: '',
|
||
accessToken: '',
|
||
accessTokenSecret: '',
|
||
callbackUrl: '',
|
||
verifier: '',
|
||
signatureMethod: 'RSA-SHA512',
|
||
privateKey: 'keys/rsa.pem',
|
||
privateKeyType: 'file',
|
||
timestamp: '',
|
||
nonce: '',
|
||
version: '1.0',
|
||
realm: '',
|
||
placement: 'header',
|
||
includeBodyHash: false
|
||
}
|
||
}
|
||
};
|
||
|
||
const bru = jsonToCollectionBru(json);
|
||
const parsed = collectionBruToJson(bru);
|
||
|
||
expect(parsed.auth.oauth1.privateKey).toBe('keys/rsa.pem');
|
||
expect(parsed.auth.oauth1.privateKeyType).toBe('file');
|
||
});
|
||
});
|