Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 191 additions & 23 deletions packages/resend/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import 'dotenv/config';
import { makeResendRequest } from './client';
import type {
ContactsCreateResponse,
ContactsDeleteResponse,
ContactsGetResponse,
ContactsListResponse,
ContactsUpdateResponse,
CreateDomainResponse,
DeleteDomainResponse,
EmailsBatchResponse,
EmailsCancelResponse,
GetDomainResponse,
GetEmailResponse,
ListDomainsResponse,
Expand All @@ -12,18 +19,20 @@ import type {
} from './endpoints/types';
import { ResendEndpointOutputSchemas } from './endpoints/types';

const TEST_API_KEY = process.env.RESEND_API_KEY!;
const TEST_API_KEY = process.env.RESEND_API_KEY;
const TEST_FROM_EMAIL =
process.env.TEST_RESEND_FROM_EMAIL || 'onboarding@resend.dev';
const TEST_TO_EMAIL =
process.env.TEST_RESEND_TO_EMAIL || 'delivered@resend.dev';

describe('Resend API Type Tests', () => {
const describeLive = TEST_API_KEY ? describe : describe.skip;

describeLive('Resend API Type Tests', () => {
describe('emails', () => {
it('emailsList returns correct type', async () => {
const response = await makeResendRequest<ListEmailsResponse>(
'emails',
TEST_API_KEY,
TEST_API_KEY!,
{ query: { limit: 10 } },
);
const result = response;
Expand All @@ -34,7 +43,7 @@ describe('Resend API Type Tests', () => {
it('emailsSend returns correct type', async () => {
const response = await makeResendRequest<SendEmailResponse>(
'emails',
TEST_API_KEY,
TEST_API_KEY!,
{
method: 'POST',
body: {
Expand All @@ -53,29 +62,90 @@ describe('Resend API Type Tests', () => {
it('emailsGet returns correct type', async () => {
const emailsListResponse = await makeResendRequest<ListEmailsResponse>(
'emails',
TEST_API_KEY,
TEST_API_KEY!,
{ query: { limit: 1 } },
);
const emailId = emailsListResponse.data[0]?.id;
if (!emailId) {
throw new Error('No emails found');
return;
}

const response = await makeResendRequest<GetEmailResponse>(
`emails/${emailId}`,
TEST_API_KEY,
TEST_API_KEY!,
);
const result = response;

ResendEndpointOutputSchemas.emailsGet.parse(result);
});

it('emailsBatch returns correct type', async () => {
const response = await makeResendRequest<EmailsBatchResponse>(
'emails/batch',
TEST_API_KEY!,
{
method: 'POST',
body: [
{
from: TEST_FROM_EMAIL,
to: [TEST_TO_EMAIL],
subject: `Batch test ${Date.now()}-1`,
html: '<p>batch 1</p>',
},
{
from: TEST_FROM_EMAIL,
to: [TEST_TO_EMAIL],
subject: `Batch test ${Date.now()}-2`,
html: '<p>batch 2</p>',
},
],
},
);
const result = response;

ResendEndpointOutputSchemas.emailsBatch.parse(result);
expect(Array.isArray(result.data)).toBe(true);
});

it('emailsCancel returns correct type', async () => {
const scheduledBody: Record<string, unknown> = {
from: TEST_FROM_EMAIL,
to: [TEST_TO_EMAIL],
subject: 'Test scheduled email',
html: '<p>Test</p>',
text: 'Test',
scheduled_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
};
const created = await makeResendRequest<SendEmailResponse>(
'emails',
TEST_API_KEY!,
{ method: 'POST', body: scheduledBody },
);
const emailId = created.id;
if (!emailId) {
return;
}

await new Promise((resolve) => setTimeout(resolve, 500));

const response = await makeResendRequest<EmailsCancelResponse>(
`emails/${emailId}/cancel`,
TEST_API_KEY!,
{ method: 'POST' },
Comment thread
greptile-apps[bot] marked this conversation as resolved.
);
const result = response;

ResendEndpointOutputSchemas.emailsCancel.parse(result);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(typeof result.id).toBe('string');
expect(result.id).toBe(emailId);
});
});

describe('domains', () => {
it('domainsList returns correct type', async () => {
const response = await makeResendRequest<ListDomainsResponse>(
'domains',
TEST_API_KEY,
TEST_API_KEY!,
{ query: { limit: 10 } },
);
const result = response;
Expand All @@ -86,7 +156,7 @@ describe('Resend API Type Tests', () => {
it('domainsGet returns correct type', async () => {
const domainsListResponse = await makeResendRequest<ListDomainsResponse>(
'domains',
TEST_API_KEY,
TEST_API_KEY!,
{
query: { limit: 1 },
},
Expand All @@ -98,18 +168,18 @@ describe('Resend API Type Tests', () => {

const response = await makeResendRequest<GetDomainResponse>(
`domains/${domainId}`,
TEST_API_KEY,
TEST_API_KEY!,
);
const result = response;

ResendEndpointOutputSchemas.domainsGet.parse(result);
});

it('domainsCreate returns correct type', async () => {
const domainName = `test-domain-${Date.now()}.example.com`;
const domainName = `test-${Date.now()}.corsair.dev`;
const response = await makeResendRequest<CreateDomainResponse>(
'domains',
TEST_API_KEY,
TEST_API_KEY!,
{
method: 'POST',
body: {
Expand All @@ -120,12 +190,18 @@ describe('Resend API Type Tests', () => {
const result = response;

ResendEndpointOutputSchemas.domainsCreate.parse(result);

if (result.id) {
await makeResendRequest(`domains/${result.id}`, TEST_API_KEY!, {
method: 'DELETE',
});
}
});

it('domainsVerify returns correct type', async () => {
const domainsListResponse = await makeResendRequest<ListDomainsResponse>(
'domains',
TEST_API_KEY,
TEST_API_KEY!,
{
query: { limit: 1 },
},
Expand All @@ -137,7 +213,7 @@ describe('Resend API Type Tests', () => {

const response = await makeResendRequest<VerifyDomainResponse>(
`domains/${domainId}/verify`,
TEST_API_KEY,
TEST_API_KEY!,
{
method: 'POST',
},
Expand All @@ -148,24 +224,23 @@ describe('Resend API Type Tests', () => {
});

it('domainsDelete returns correct type', async () => {
const domainsListResponse = await makeResendRequest<ListDomainsResponse>(
const domainName = `test-delete-${Date.now()}.corsair.dev`;
const created = await makeResendRequest<CreateDomainResponse>(
'domains',
TEST_API_KEY,
TEST_API_KEY!,
{
query: { limit: 10 },
method: 'POST',
body: { name: domainName },
},
);
const testDomain = domainsListResponse.data.find((domain) =>
domain.name.startsWith('test-domain-'),
);
const domainId = testDomain?.id;
const domainId = created?.id;
if (!domainId) {
throw new Error('No test domain found for deletion');
return;
}

const response = await makeResendRequest<DeleteDomainResponse>(
`domains/${domainId}`,
TEST_API_KEY,
TEST_API_KEY!,
{
method: 'DELETE',
},
Expand All @@ -175,4 +250,97 @@ describe('Resend API Type Tests', () => {
ResendEndpointOutputSchemas.domainsDelete.parse(result);
});
});

describe('contacts', () => {
const testContactEmail = `corsair-test+${Date.now()}@example.com`;
let createdContactId: string;

it('contactsCreate returns correct type', async () => {
const response = await makeResendRequest<ContactsCreateResponse>(
'contacts',
TEST_API_KEY!,
{
method: 'POST',
body: {
email: testContactEmail,
first_name: 'Corsair',
last_name: 'Test',
unsubscribed: false,
},
},
);
const result = response;

ResendEndpointOutputSchemas.contactsCreate.parse(result);
expect(typeof result.id).toBe('string');
expect(result.id.length).toBeGreaterThan(0);
createdContactId = result.id;
});

it('contactsList returns correct type', async () => {
const response = await makeResendRequest<ContactsListResponse>(
'contacts',
TEST_API_KEY!,
{ query: { limit: 10 } },
);
const result = response;

ResendEndpointOutputSchemas.contactsList.parse(result);
expect(Array.isArray(result.data)).toBe(true);
});

it('contactsGet returns correct type', async () => {
expect(createdContactId).toBeDefined();

const response = await makeResendRequest<ContactsGetResponse>(
`contacts/${createdContactId}`,
TEST_API_KEY!,
);
const result = response;

ResendEndpointOutputSchemas.contactsGet.parse(result);
expect(result.id).toBe(createdContactId);
expect(result.email).toBe(testContactEmail);
});

it('contactsUpdate returns correct type and persists first_name', async () => {
expect(createdContactId).toBeDefined();

const response = await makeResendRequest<ContactsUpdateResponse>(
`contacts/${createdContactId}`,
TEST_API_KEY!,
{
method: 'PATCH',
body: {
first_name: 'CorsairUpdated',
},
},
);
const result = response;

ResendEndpointOutputSchemas.contactsUpdate.parse(result);

const getResponse = await makeResendRequest<ContactsGetResponse>(
`contacts/${createdContactId}`,
TEST_API_KEY!,
);
const fetched = getResponse;
expect(fetched.id).toBe(createdContactId);
expect(fetched.first_name).toBe('CorsairUpdated');
});

it('contactsDelete returns correct type', async () => {
expect(createdContactId).toBeDefined();

const response = await makeResendRequest<ContactsDeleteResponse>(
`contacts/${createdContactId}`,
TEST_API_KEY!,
{ method: 'DELETE' },
);
const result = response;

ResendEndpointOutputSchemas.contactsDelete.parse(result);
expect(result.deleted).toBe(true);
});
});
});
25 changes: 22 additions & 3 deletions packages/resend/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http';
import { BaseWebhookHandler, request, verifyHmacSignature } from 'corsair/http';
import {
ApiError,
BaseWebhookHandler,
request,
verifyHmacSignature,
} from 'corsair/http';
import type {
ResendEventMap,
ResendEventName,
Expand All @@ -10,6 +15,8 @@ export class ResendAPIError extends Error {
constructor(
message: string,
public readonly code?: string,
public readonly status?: number,
public readonly body?: unknown,
) {
super(message);
this.name = 'ResendAPIError';
Expand All @@ -23,7 +30,7 @@ export async function makeResendRequest<T>(
apiKey: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
body?: Record<string, unknown>;
body?: unknown;
query?: Record<string, string | number | boolean | undefined>;
} = {},
): Promise<T> {
Expand Down Expand Up @@ -54,7 +61,19 @@ export async function makeResendRequest<T>(
try {
const response = await request<T>(config, requestOptions);
return response;
} catch (error) {
} catch (error: any) {
if (error instanceof ApiError) {
const msg =
typeof error.body === 'object' && error.body && 'message' in error.body
? String((error.body as any).message)
: error.message;
throw new ResendAPIError(
msg || error.message,
(error.body as any)?.name || (error.body as any)?.code,
error.status,
error.body,
);
}
if (error instanceof Error) {
throw new ResendAPIError(error.message);
}
Expand Down
Loading
Loading