diff --git a/src/index.ts b/src/index.ts index d3e97719..ab2f8c38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ export * from './emails/attachments/interfaces'; export * from './emails/interfaces'; export * from './emails/receiving/interfaces'; export type { ErrorResponse, Response } from './interfaces'; -export { Resend } from './resend'; +export { Resend, type ResendOptions } from './resend'; export * from './segments/interfaces'; export * from './templates/interfaces'; export * from './topics/interfaces'; diff --git a/src/resend.spec.ts b/src/resend.spec.ts new file mode 100644 index 00000000..7935b658 --- /dev/null +++ b/src/resend.spec.ts @@ -0,0 +1,135 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from './resend'; +import { mockSuccessResponse } from './test-utils/mock-fetch'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('Resend', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('constructor options', () => { + it('uses default baseUrl and userAgent when no options provided', () => { + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + expect(resend.baseUrl).toBe('https://api.resend.com'); + expect(resend.userAgent).toMatch(/^resend-node:/); + }); + + it('uses custom baseUrl when options.baseUrl is provided', () => { + const customBaseUrl = 'https://eu.api.resend.com'; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + baseUrl: customBaseUrl, + }); + + expect(resend.baseUrl).toBe(customBaseUrl); + }); + + it('uses custom userAgent when options.userAgent is provided', () => { + const customUserAgent = 'my-app/1.0'; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + userAgent: customUserAgent, + }); + + expect(resend.userAgent).toBe(customUserAgent); + }); + + it('uses both custom baseUrl and userAgent when both provided', () => { + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + baseUrl: 'https://custom.api.com', + userAgent: 'custom-agent/2.0', + }); + + expect(resend.baseUrl).toBe('https://custom.api.com'); + expect(resend.userAgent).toBe('custom-agent/2.0'); + }); + + it('uses RESEND_BASE_URL from env when no options.baseUrl provided', () => { + const originalEnv = process.env; + process.env = { + ...originalEnv, + RESEND_BASE_URL: 'https://env-base-url.example.com', + }; + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + expect(resend.baseUrl).toBe('https://env-base-url.example.com'); + + process.env = originalEnv; + }); + + it('uses RESEND_USER_AGENT from env when no options.userAgent provided', () => { + const originalEnv = process.env; + process.env = { + ...originalEnv, + RESEND_USER_AGENT: 'env-user-agent/1.0', + }; + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + expect(resend.userAgent).toBe('env-user-agent/1.0'); + + process.env = originalEnv; + }); + + it('options.baseUrl overrides RESEND_BASE_URL env', () => { + const originalEnv = process.env; + process.env = { + ...originalEnv, + RESEND_BASE_URL: 'https://env-base-url.example.com', + }; + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + baseUrl: 'https://options-base-url.example.com', + }); + expect(resend.baseUrl).toBe('https://options-base-url.example.com'); + + process.env = originalEnv; + }); + + it('options.userAgent overrides RESEND_USER_AGENT env', () => { + const originalEnv = process.env; + process.env = { + ...originalEnv, + RESEND_USER_AGENT: 'env-user-agent/1.0', + }; + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + userAgent: 'options-user-agent/2.0', + }); + expect(resend.userAgent).toBe('options-user-agent/2.0'); + + process.env = originalEnv; + }); + }); + + describe('fetchRequest with custom options', () => { + it('sends request to custom baseUrl', async () => { + const customBaseUrl = 'https://custom.api.resend.com'; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + baseUrl: customBaseUrl, + }); + + mockSuccessResponse({ id: 'key-123' }, { headers: {} }); + + await resend.apiKeys.list(); + + const [url] = fetchMock.mock.calls[0]; + expect(url).toBe(`${customBaseUrl}/api-keys`); + }); + + it('sends custom User-Agent in request headers', async () => { + const customUserAgent = 'my-integration/3.0'; + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop', { + userAgent: customUserAgent, + }); + + mockSuccessResponse({ id: 'key-123' }, { headers: {} }); + + await resend.apiKeys.list(); + + const requestOptions = fetchMock.mock.calls[0][1]; + const headers = requestOptions?.headers as Headers; + expect(headers.get('User-Agent')).toBe(customUserAgent); + }); + }); +}); diff --git a/src/resend.ts b/src/resend.ts index a19f3754..e206da37 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -17,16 +17,27 @@ import { Webhooks } from './webhooks/webhooks'; const defaultBaseUrl = 'https://api.resend.com'; const defaultUserAgent = `resend-node:${version}`; -const baseUrl = - typeof process !== 'undefined' && process.env + +function getDefaultBaseUrl(): string { + return typeof process !== 'undefined' && process.env ? process.env.RESEND_BASE_URL || defaultBaseUrl : defaultBaseUrl; -const userAgent = - typeof process !== 'undefined' && process.env +} + +function getDefaultUserAgent(): string { + return typeof process !== 'undefined' && process.env ? process.env.RESEND_USER_AGENT || defaultUserAgent : defaultUserAgent; +} + +export interface ResendOptions { + baseUrl?: string; + userAgent?: string; +} export class Resend { + readonly baseUrl: string; + readonly userAgent: string; private readonly headers: Headers; readonly apiKeys = new ApiKeys(this); @@ -45,7 +56,10 @@ export class Resend { readonly templates = new Templates(this); readonly topics = new Topics(this); - constructor(readonly key?: string) { + constructor( + readonly key?: string, + options?: ResendOptions, + ) { if (!key) { if (typeof process !== 'undefined' && process.env) { this.key = process.env.RESEND_API_KEY; @@ -58,16 +72,19 @@ export class Resend { } } + this.baseUrl = options?.baseUrl ?? getDefaultBaseUrl(); + this.userAgent = options?.userAgent ?? getDefaultUserAgent(); + this.headers = new Headers({ Authorization: `Bearer ${this.key}`, - 'User-Agent': userAgent, + 'User-Agent': this.userAgent, 'Content-Type': 'application/json', }); } async fetchRequest(path: string, options = {}): Promise> { try { - const response = await fetch(`${baseUrl}${path}`, options); + const response = await fetch(`${this.baseUrl}${path}`, options); if (!response.ok) { try {