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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Currently, with this SDK you can:
- Contacts CRUD
- Lists CRUD
- Contact Fields CRUD
- Contact Imports (bulk import contacts)
- General
- Templates CRUD
- Suppressions management (find and delete)
Expand Down Expand Up @@ -126,6 +127,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other

- [Contact Fields](examples/contact-fields/everything.ts)

### Contact Imports API

- [Contact Imports](examples/contact-imports/everything.ts)

### Sending API

- [Advanced](examples/sending/everything.ts)
Expand Down
36 changes: 36 additions & 0 deletions examples/contact-imports/everything.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MailtrapClient } from "mailtrap";

const TOKEN = "<YOUR-TOKEN-HERE>";
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";

const client = new MailtrapClient({
token: TOKEN,
accountId: ACCOUNT_ID
});

async function runContactImportsFlow() {
const importData = {
contacts: [
{
email: "[email protected]"
},
{
email: "[email protected]"
}
]
};

try {
// Create import
const response = await client.contactImports.create(importData);
console.log("Import created:", response);

// Get import by ID
const importDetails = await client.contactImports.get(response.id);
console.log("Import details:", importDetails);
} catch (error: any) {
console.error("Error:", error);
}
}

runContactImportsFlow();
17 changes: 17 additions & 0 deletions src/__tests__/lib/api/ContactImports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import axios from "axios";

import ContactImports from "../../../lib/api/ContactImports";

describe("lib/api/ContactImports: ", () => {
const accountId = 100;
const contactImportsAPI = new ContactImports(axios, accountId);

describe("class ContactImports(): ", () => {
describe("init: ", () => {
it("initializes with all necessary params.", () => {
expect(contactImportsAPI).toHaveProperty("create");
expect(contactImportsAPI).toHaveProperty("get");
});
});
});
});
265 changes: 265 additions & 0 deletions src/__tests__/lib/api/resources/ContactImports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import axios from "axios";
import AxiosMockAdapter from "axios-mock-adapter";

import ContactImportsApi from "../../../../lib/api/resources/ContactImports";
import handleSendingError from "../../../../lib/axios-logger";
import MailtrapError from "../../../../lib/MailtrapError";
import {
ContactImportResponse,
ImportContactsRequest,
} from "../../../../types/api/contact-imports";

import CONFIG from "../../../../config";

const { CLIENT_SETTINGS } = CONFIG;
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;

describe("lib/api/resources/ContactImports: ", () => {
let mock: AxiosMockAdapter;
const accountId = 100;
const contactImportsAPI = new ContactImportsApi(axios, accountId);

const createImportRequest: ImportContactsRequest = {
contacts: [
{
email: "[email protected]",
fields: {
first_name: "John",
last_name: "Doe",
},
list_ids_included: [1, 2],
},
{
email: "[email protected]",
fields: {
first_name: "Jane",
zip_code: 12345,
},
list_ids_excluded: [3],
},
],
};

const createImportResponse: ContactImportResponse = {
id: 1,
status: "created",
created_contacts_count: 2,
updated_contacts_count: 0,
contacts_over_limit_count: 0,
};

const getImportResponse: ContactImportResponse = {
id: 1,
status: "finished",
created_contacts_count: 2,
updated_contacts_count: 0,
contacts_over_limit_count: 0,
};

describe("class ContactImportsApi(): ", () => {
describe("init: ", () => {
it("initializes with all necessary params.", () => {
expect(contactImportsAPI).toHaveProperty("create");
expect(contactImportsAPI).toHaveProperty("get");
});
});
});

beforeAll(() => {
axios.interceptors.response.use(
(response) => response.data,
handleSendingError
);
mock = new AxiosMockAdapter(axios);
});

afterEach(() => {
mock.reset();
});

describe("create(): ", () => {
it("successfully creates a contact import.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
const expectedResponseData = createImportResponse;

expect.assertions(2);

mock
.onPost(endpoint, createImportRequest)
.reply(200, expectedResponseData);
const result = await contactImportsAPI.create(createImportRequest);

expect(mock.history.post[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("successfully creates a contact import with minimal data.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
const minimalRequest: ImportContactsRequest = {
contacts: [
{
email: "[email protected]",
},
],
};
const expectedResponseData: ContactImportResponse = {
id: 2,
status: "created",
};

expect.assertions(2);

mock.onPost(endpoint, minimalRequest).reply(200, expectedResponseData);
const result = await contactImportsAPI.create(minimalRequest);

expect(mock.history.post[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("fails with error.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
const expectedErrorMessage = "Request failed with status code 400";

expect.assertions(2);

mock.onPost(endpoint).reply(400, { error: expectedErrorMessage });

try {
await contactImportsAPI.create(createImportRequest);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});

it("fails with validation error.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
const invalidRequest: ImportContactsRequest = {
contacts: [
{
email: "invalid-email",
},
],
};
const expectedErrorMessage = "Invalid email format";

expect.assertions(2);

mock.onPost(endpoint).reply(422, { error: expectedErrorMessage });

try {
await contactImportsAPI.create(invalidRequest);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});

it("fails with array of validation errors.", async () => {
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
const invalidRequest: ImportContactsRequest = {
contacts: [
{
email: "invalid-email-1",
},
{
email: "invalid-email-2",
},
],
};

expect.assertions(2);

// API returns errors as an array of objects (confirmed by actual API response)
// Each object contains the email and nested errors object with field-specific messages
mock.onPost(endpoint).reply(422, {
errors: [
{
email: "invalid-email-1",
errors: {
email: ["is invalid", "is required"],
},
},
{
email: "invalid-email-2",
errors: {
base: ["Contact limit exceeded"],
},
},
],
});

try {
await contactImportsAPI.create(invalidRequest);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
// Note: Current axios-logger doesn't properly handle array of objects format,
// so it falls back to stringifying the array, resulting in [object Object],[object Object]
// This test documents the current behavior. Updating axios-logger to properly
// parse this format will be a separate task.
expect(error.message).toBe("[object Object],[object Object]");
}
}
});
});

describe("get(): ", () => {
it("successfully gets a contact import by ID.", async () => {
const importId = 1;
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
const expectedResponseData = getImportResponse;

expect.assertions(2);

mock.onGet(endpoint).reply(200, expectedResponseData);
const result = await contactImportsAPI.get(importId);

expect(mock.history.get[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("successfully gets a contact import with all status fields.", async () => {
const importId = 2;
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
const expectedResponseData: ContactImportResponse = {
id: importId,
status: "failed",
created_contacts_count: 5,
updated_contacts_count: 3,
contacts_over_limit_count: 2,
};

expect.assertions(2);

mock.onGet(endpoint).reply(200, expectedResponseData);
const result = await contactImportsAPI.get(importId);

expect(mock.history.get[0].url).toEqual(endpoint);
expect(result).toEqual(expectedResponseData);
});

it("fails with error when getting a contact import.", async () => {
const importId = 999;
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
const expectedErrorMessage = "Contact import not found";

expect.assertions(2);

mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });

try {
await contactImportsAPI.get(importId);
} catch (error) {
expect(error).toBeInstanceOf(MailtrapError);
if (error instanceof MailtrapError) {
expect(error.message).toEqual(expectedErrorMessage);
}
}
});
});
});
20 changes: 14 additions & 6 deletions src/lib/MailtrapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import encodeMailBuffers from "./mail-buffer-encoder";
import handleSendingError from "./axios-logger";
import MailtrapError from "./MailtrapError";

import GeneralAPI from "./api/General";
import TestingAPI from "./api/Testing";
import ContactsBaseAPI from "./api/Contacts";
import ContactListsBaseAPI from "./api/ContactLists";
import ContactFieldsBaseAPI from "./api/ContactFields";
import ContactListsBaseAPI from "./api/ContactLists";
import ContactImportsBaseAPI from "./api/ContactImports";
import GeneralAPI from "./api/General";
import TemplatesBaseAPI from "./api/Templates";
import SuppressionsBaseAPI from "./api/Suppressions";
import SendingDomainsBaseAPI from "./api/SendingDomains";
import TestingAPI from "./api/Testing";

import CONFIG from "../config";

Expand Down Expand Up @@ -146,6 +147,14 @@ export default class MailtrapClient {
return new ContactFieldsBaseAPI(this.axios, accountId);
}

/**
* Getter for Contact Imports API.
*/
get contactImports() {
const accountId = this.validateAccountIdPresence();
return new ContactImportsBaseAPI(this.axios, accountId);
}

/**
* Getter for Templates API.
*/
Expand All @@ -166,9 +175,8 @@ export default class MailtrapClient {
* Getter for Sending Domains API.
*/
get sendingDomains() {
this.validateAccountIdPresence();

return new SendingDomainsBaseAPI(this.axios, this.accountId!);
const accountId = this.validateAccountIdPresence();
return new SendingDomainsBaseAPI(this.axios, accountId);
}

/**
Expand Down
Loading