diff --git a/src/Sage.ts b/src/Sage.ts index 45ed25a..a2e79ea 100644 --- a/src/Sage.ts +++ b/src/Sage.ts @@ -13,7 +13,8 @@ import { statusCodeToMessage, parseJsonStr, isRedirect, - isError + isError, + wrapArray } from './utils.js'; import { SageHttpResponse } from './SageHttpResponse.js'; import path from 'node:path'; @@ -69,6 +70,10 @@ export class Sage { this.client = new Client(`http://localhost:${port}`, httpClientOptions); } + /** + * Sets query parameters for the request. + * @param query + */ query(query: Record): this { this.request.query = query; return this; @@ -104,28 +109,54 @@ export class Sage { * @param key * @param value */ - set(key: string, value: string): this { - this.request.headers = { ...(this.request.headers || {}), [key]: value }; + set(key: string, value: string | string[]): this { + if (!this.request.headers) { + this.request.headers = {}; + } + + value = wrapArray(value); + + // If already an array + if (Array.isArray(this.request.headers[key])) { + const existingValue = this.request.headers[key] as string[]; + existingValue.push(...value); + return this; + } + + // If an existing value is a string, convert it to an array + if (typeof this.request.headers[key] === 'string') { + const existingValue = this.request.headers[key] as string; + this.request.headers[key] = [existingValue, ...value]; + return this; + } + + // If a single value is passed, don't wrap it in an array + if (value.length === 1) { + this.request.headers[key] = value[0]; + return this; + } + + this.request.headers[key] = value; return this; } /** - * Sets the Authorization header to base64 of Bearer token with a Basic Prefix. - * @param username + * If password is provided, it will be used to create a Basic Auth header. + * If password is not provided, it will be used as a Bearer token. + * Automatically adds Basic or Bearer prefix to the token. + * @param usernameOrToken * @param password */ - basic(username: string, password: string): this { - const encoded = Buffer.from(`${username}:${password}`).toString('base64'); - this.set('Authorization', `Basic ${encoded}`); - return this; - } + auth(usernameOrToken: string, password?: string): this { + if (password) { + const credentials = Buffer.from( + `${usernameOrToken}:${password}` + ).toString('base64'); + this.set('Authorization', `Basic ${credentials}`); + return this; + } - /** - * Sets the Authorization header to Bearer token. The prefix will be added. - * @param token - */ - bearer(token: string): this { - this.set('Authorization', `Bearer ${token}`); + this.set('Authorization', `Bearer ${usernameOrToken}`); return this; } @@ -254,7 +285,24 @@ export class Sage { } satisfies SageHttpResponse); } catch (e) { throw new SageException( - `Failed to make a request to the underlying server, please take a look at the upstream error for more details: `, + ` + Failed; + to; + make; + a; + request; + to; + the; + underlying; + server, please; + take; + a; + look; + at; + the; + upstream; + error; + for more details: `, e ); } finally { diff --git a/src/utils.ts b/src/utils.ts index c53039d..cc3db60 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -78,3 +78,11 @@ export const isObject = (candidate: unknown): candidate is object => { export const copyObject = (obj: T): T => { return JSON.parse(JSON.stringify(obj)); }; + +export const wrapArray = (value: T | T[]): T[] => { + if (Array.isArray(value)) { + return value; + } + + return [value]; +}; diff --git a/test/Sage.spec.ts b/test/Sage.spec.ts index fd5188b..fca0105 100644 --- a/test/Sage.spec.ts +++ b/test/Sage.spec.ts @@ -1,12 +1,16 @@ import { createServer } from 'node:http'; import { Sage } from '../src/Sage.js'; import { getExpressApp, getFastifyApp } from './utils.js'; +import { ConfigStore } from '../src/ConfigStore.js'; +import { SAGE_DEFAULT_CONFIG } from '../src/index.js'; describe('Sage', () => { it('should initialize sage assistant', async () => { const expressServer = createServer(getExpressApp()); const fastifyServer = getFastifyApp(); const server = createServer(); + const store = new ConfigStore(SAGE_DEFAULT_CONFIG); + const config = store.getConfig(); const sage1 = new Sage( { @@ -18,7 +22,8 @@ describe('Sage', () => { launched: false }, 'GET', - '/test' + '/test', + config ); const sage2 = new Sage( { @@ -30,7 +35,8 @@ describe('Sage', () => { launched: false }, 'GET', - '/test' + '/test', + config ); const sage3 = new Sage( { @@ -40,9 +46,9 @@ describe('Sage', () => { launched: false }, 'GET', - '/test' + '/test', + config ); - expect(sage1).toBeTruthy(); expect(sage2).toBeTruthy(); expect(sage3).toBeTruthy(); diff --git a/test/index.spec.ts b/test/index.spec.ts index 64cabad..3947fd0 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -372,6 +372,70 @@ describe('request', () => { }); }); }); + describe('header', () => { + it('should set and pass a single header', async () => { + const res = await request(expressApp) + .post('/ping-pong') + .set('x-custom-header', 'custom-value'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value' + } + } + } as SageHttpResponse); + }); + + it('should set and pass 2 values in a single header', async () => { + const res = await request(expressApp) + .post('/ping-pong') + .set('x-custom-header', 'custom-value1') + .set('x-custom-header', 'custom-value2'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2' + } + } + } as SageHttpResponse); + }); + + it('should set and pass 3 values in a single header', async () => { + const res = await request(expressApp) + .post('/ping-pong') + .set('x-custom-header', 'custom-value1') + .set('x-custom-header', 'custom-value2') + .set('x-custom-header', 'custom-value3'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2, custom-value3' + } + } + } as SageHttpResponse); + }); + + it('should accept arrays', async () => { + const res = await request(expressApp) + .post('/ping-pong') + .set('x-custom-header', [ + 'custom-value1', + 'custom-value2', + 'custom-value3' + ]); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2, custom-value3' + } + } + } as SageHttpResponse); + }); + }); }); describe('fastify', () => { @@ -604,5 +668,84 @@ describe('request', () => { } as SageHttpResponse); }); }); + + describe('header', () => { + it('should set and pass a single header', async () => { + const res = await request(fastifyApp.server) + .post('/ping-pong') + .set('x-custom-header', 'custom-value'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value' + } + } + } as SageHttpResponse); + }); + + it('should set and pass 2 values in a single header', async () => { + const res = await request(fastifyApp.server) + .post('/ping-pong') + .set('x-custom-header', 'custom-value1') + .set('x-custom-header', 'custom-value2'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2' + } + } + } as SageHttpResponse); + }); + + it('should set and pass 3 values in a single header', async () => { + const res = await request(fastifyApp.server) + .post('/ping-pong') + .set('x-custom-header', 'custom-value1') + .set('x-custom-header', 'custom-value2') + .set('x-custom-header', 'custom-value3'); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2, custom-value3' + } + } + } as SageHttpResponse); + }); + + it('should accept array with multiple values', async () => { + const res = await request(fastifyApp.server) + .post('/ping-pong') + .set('x-custom-header', [ + 'custom-value1', + 'custom-value2', + 'custom-value3' + ]); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1, custom-value2, custom-value3' + } + } + } as SageHttpResponse); + }); + + it('should accept arrays with a single value', async () => { + const res = await request(fastifyApp.server) + .post('/ping-pong') + .set('x-custom-header', ['custom-value1']); + + expect(res).toMatchObject({ + body: { + reqHeaders: { + 'x-custom-header': 'custom-value1' + } + } + } as SageHttpResponse); + }); + }); }); });