Skip to content

Commit e4dded0

Browse files
Merge pull request #95 from mailtrap/contact-imports
Contact imports
2 parents 00c7d79 + 560cd60 commit e4dded0

File tree

8 files changed

+416
-6
lines changed

8 files changed

+416
-6
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Currently, with this SDK you can:
3333
- Contacts CRUD
3434
- Lists CRUD
3535
- Contact Fields CRUD
36+
- Contact Imports (bulk import contacts)
3637
- General
3738
- Templates CRUD
3839
- Suppressions management (find and delete)
@@ -126,6 +127,10 @@ Refer to the [`examples`](examples) folder for the source code of this and other
126127

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

130+
### Contact Imports API
131+
132+
- [Contact Imports](examples/contact-imports/everything.ts)
133+
129134
### Sending API
130135

131136
- [Advanced](examples/sending/everything.ts)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { MailtrapClient } from "mailtrap";
2+
3+
const TOKEN = "<YOUR-TOKEN-HERE>";
4+
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>";
5+
6+
const client = new MailtrapClient({
7+
token: TOKEN,
8+
accountId: ACCOUNT_ID
9+
});
10+
11+
async function runContactImportsFlow() {
12+
const importData = {
13+
contacts: [
14+
{
15+
16+
},
17+
{
18+
19+
}
20+
]
21+
};
22+
23+
try {
24+
// Create import
25+
const response = await client.contactImports.create(importData);
26+
console.log("Import created:", response);
27+
28+
// Get import by ID
29+
const importDetails = await client.contactImports.get(response.id);
30+
console.log("Import details:", importDetails);
31+
} catch (error: any) {
32+
console.error("Error:", error);
33+
}
34+
}
35+
36+
runContactImportsFlow();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import axios from "axios";
2+
3+
import ContactImports from "../../../lib/api/ContactImports";
4+
5+
describe("lib/api/ContactImports: ", () => {
6+
const accountId = 100;
7+
const contactImportsAPI = new ContactImports(axios, accountId);
8+
9+
describe("class ContactImports(): ", () => {
10+
describe("init: ", () => {
11+
it("initializes with all necessary params.", () => {
12+
expect(contactImportsAPI).toHaveProperty("create");
13+
expect(contactImportsAPI).toHaveProperty("get");
14+
});
15+
});
16+
});
17+
});
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import axios from "axios";
2+
import AxiosMockAdapter from "axios-mock-adapter";
3+
4+
import ContactImportsApi from "../../../../lib/api/resources/ContactImports";
5+
import handleSendingError from "../../../../lib/axios-logger";
6+
import MailtrapError from "../../../../lib/MailtrapError";
7+
import {
8+
ContactImportResponse,
9+
ImportContactsRequest,
10+
} from "../../../../types/api/contact-imports";
11+
12+
import CONFIG from "../../../../config";
13+
14+
const { CLIENT_SETTINGS } = CONFIG;
15+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
16+
17+
describe("lib/api/resources/ContactImports: ", () => {
18+
let mock: AxiosMockAdapter;
19+
const accountId = 100;
20+
const contactImportsAPI = new ContactImportsApi(axios, accountId);
21+
22+
const createImportRequest: ImportContactsRequest = {
23+
contacts: [
24+
{
25+
26+
fields: {
27+
first_name: "John",
28+
last_name: "Doe",
29+
},
30+
list_ids_included: [1, 2],
31+
},
32+
{
33+
34+
fields: {
35+
first_name: "Jane",
36+
zip_code: 12345,
37+
},
38+
list_ids_excluded: [3],
39+
},
40+
],
41+
};
42+
43+
const createImportResponse: ContactImportResponse = {
44+
id: 1,
45+
status: "created",
46+
created_contacts_count: 2,
47+
updated_contacts_count: 0,
48+
contacts_over_limit_count: 0,
49+
};
50+
51+
const getImportResponse: ContactImportResponse = {
52+
id: 1,
53+
status: "finished",
54+
created_contacts_count: 2,
55+
updated_contacts_count: 0,
56+
contacts_over_limit_count: 0,
57+
};
58+
59+
describe("class ContactImportsApi(): ", () => {
60+
describe("init: ", () => {
61+
it("initializes with all necessary params.", () => {
62+
expect(contactImportsAPI).toHaveProperty("create");
63+
expect(contactImportsAPI).toHaveProperty("get");
64+
});
65+
});
66+
});
67+
68+
beforeAll(() => {
69+
axios.interceptors.response.use(
70+
(response) => response.data,
71+
handleSendingError
72+
);
73+
mock = new AxiosMockAdapter(axios);
74+
});
75+
76+
afterEach(() => {
77+
mock.reset();
78+
});
79+
80+
describe("create(): ", () => {
81+
it("successfully creates a contact import.", async () => {
82+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
83+
const expectedResponseData = createImportResponse;
84+
85+
expect.assertions(2);
86+
87+
mock
88+
.onPost(endpoint, createImportRequest)
89+
.reply(200, expectedResponseData);
90+
const result = await contactImportsAPI.create(createImportRequest);
91+
92+
expect(mock.history.post[0].url).toEqual(endpoint);
93+
expect(result).toEqual(expectedResponseData);
94+
});
95+
96+
it("successfully creates a contact import with minimal data.", async () => {
97+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
98+
const minimalRequest: ImportContactsRequest = {
99+
contacts: [
100+
{
101+
102+
},
103+
],
104+
};
105+
const expectedResponseData: ContactImportResponse = {
106+
id: 2,
107+
status: "created",
108+
};
109+
110+
expect.assertions(2);
111+
112+
mock.onPost(endpoint, minimalRequest).reply(200, expectedResponseData);
113+
const result = await contactImportsAPI.create(minimalRequest);
114+
115+
expect(mock.history.post[0].url).toEqual(endpoint);
116+
expect(result).toEqual(expectedResponseData);
117+
});
118+
119+
it("fails with error.", async () => {
120+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
121+
const expectedErrorMessage = "Request failed with status code 400";
122+
123+
expect.assertions(2);
124+
125+
mock.onPost(endpoint).reply(400, { error: expectedErrorMessage });
126+
127+
try {
128+
await contactImportsAPI.create(createImportRequest);
129+
} catch (error) {
130+
expect(error).toBeInstanceOf(MailtrapError);
131+
if (error instanceof MailtrapError) {
132+
expect(error.message).toEqual(expectedErrorMessage);
133+
}
134+
}
135+
});
136+
137+
it("fails with validation error.", async () => {
138+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
139+
const invalidRequest: ImportContactsRequest = {
140+
contacts: [
141+
{
142+
email: "invalid-email",
143+
},
144+
],
145+
};
146+
const expectedErrorMessage = "Invalid email format";
147+
148+
expect.assertions(2);
149+
150+
mock.onPost(endpoint).reply(422, { error: expectedErrorMessage });
151+
152+
try {
153+
await contactImportsAPI.create(invalidRequest);
154+
} catch (error) {
155+
expect(error).toBeInstanceOf(MailtrapError);
156+
if (error instanceof MailtrapError) {
157+
expect(error.message).toEqual(expectedErrorMessage);
158+
}
159+
}
160+
});
161+
162+
it("fails with array of validation errors.", async () => {
163+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`;
164+
const invalidRequest: ImportContactsRequest = {
165+
contacts: [
166+
{
167+
email: "invalid-email-1",
168+
},
169+
{
170+
email: "invalid-email-2",
171+
},
172+
],
173+
};
174+
175+
expect.assertions(2);
176+
177+
// API returns errors as an array of objects (confirmed by actual API response)
178+
// Each object contains the email and nested errors object with field-specific messages
179+
mock.onPost(endpoint).reply(422, {
180+
errors: [
181+
{
182+
email: "invalid-email-1",
183+
errors: {
184+
email: ["is invalid", "is required"],
185+
},
186+
},
187+
{
188+
email: "invalid-email-2",
189+
errors: {
190+
base: ["Contact limit exceeded"],
191+
},
192+
},
193+
],
194+
});
195+
196+
try {
197+
await contactImportsAPI.create(invalidRequest);
198+
} catch (error) {
199+
expect(error).toBeInstanceOf(MailtrapError);
200+
if (error instanceof MailtrapError) {
201+
// Note: Current axios-logger doesn't properly handle array of objects format,
202+
// so it falls back to stringifying the array, resulting in [object Object],[object Object]
203+
// This test documents the current behavior. Updating axios-logger to properly
204+
// parse this format will be a separate task.
205+
expect(error.message).toBe("[object Object],[object Object]");
206+
}
207+
}
208+
});
209+
});
210+
211+
describe("get(): ", () => {
212+
it("successfully gets a contact import by ID.", async () => {
213+
const importId = 1;
214+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
215+
const expectedResponseData = getImportResponse;
216+
217+
expect.assertions(2);
218+
219+
mock.onGet(endpoint).reply(200, expectedResponseData);
220+
const result = await contactImportsAPI.get(importId);
221+
222+
expect(mock.history.get[0].url).toEqual(endpoint);
223+
expect(result).toEqual(expectedResponseData);
224+
});
225+
226+
it("successfully gets a contact import with all status fields.", async () => {
227+
const importId = 2;
228+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
229+
const expectedResponseData: ContactImportResponse = {
230+
id: importId,
231+
status: "failed",
232+
created_contacts_count: 5,
233+
updated_contacts_count: 3,
234+
contacts_over_limit_count: 2,
235+
};
236+
237+
expect.assertions(2);
238+
239+
mock.onGet(endpoint).reply(200, expectedResponseData);
240+
const result = await contactImportsAPI.get(importId);
241+
242+
expect(mock.history.get[0].url).toEqual(endpoint);
243+
expect(result).toEqual(expectedResponseData);
244+
});
245+
246+
it("fails with error when getting a contact import.", async () => {
247+
const importId = 999;
248+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports/${importId}`;
249+
const expectedErrorMessage = "Contact import not found";
250+
251+
expect.assertions(2);
252+
253+
mock.onGet(endpoint).reply(404, { error: expectedErrorMessage });
254+
255+
try {
256+
await contactImportsAPI.get(importId);
257+
} catch (error) {
258+
expect(error).toBeInstanceOf(MailtrapError);
259+
if (error instanceof MailtrapError) {
260+
expect(error.message).toEqual(expectedErrorMessage);
261+
}
262+
}
263+
});
264+
});
265+
});

src/lib/MailtrapClient.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import encodeMailBuffers from "./mail-buffer-encoder";
77
import handleSendingError from "./axios-logger";
88
import MailtrapError from "./MailtrapError";
99

10-
import GeneralAPI from "./api/General";
11-
import TestingAPI from "./api/Testing";
1210
import ContactsBaseAPI from "./api/Contacts";
13-
import ContactListsBaseAPI from "./api/ContactLists";
1411
import ContactFieldsBaseAPI from "./api/ContactFields";
12+
import ContactListsBaseAPI from "./api/ContactLists";
13+
import ContactImportsBaseAPI from "./api/ContactImports";
14+
import GeneralAPI from "./api/General";
1515
import TemplatesBaseAPI from "./api/Templates";
1616
import SuppressionsBaseAPI from "./api/Suppressions";
1717
import SendingDomainsBaseAPI from "./api/SendingDomains";
18+
import TestingAPI from "./api/Testing";
1819

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

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

150+
/**
151+
* Getter for Contact Imports API.
152+
*/
153+
get contactImports() {
154+
const accountId = this.validateAccountIdPresence();
155+
return new ContactImportsBaseAPI(this.axios, accountId);
156+
}
157+
149158
/**
150159
* Getter for Templates API.
151160
*/
@@ -166,9 +175,8 @@ export default class MailtrapClient {
166175
* Getter for Sending Domains API.
167176
*/
168177
get sendingDomains() {
169-
this.validateAccountIdPresence();
170-
171-
return new SendingDomainsBaseAPI(this.axios, this.accountId!);
178+
const accountId = this.validateAccountIdPresence();
179+
return new SendingDomainsBaseAPI(this.axios, accountId);
172180
}
173181

174182
/**

0 commit comments

Comments
 (0)