Skip to content

Commit 3e90cfa

Browse files
Merge branch 'main' into contact-exports-api
2 parents 9d85c00 + e4dded0 commit 3e90cfa

File tree

14 files changed

+615
-19
lines changed

14 files changed

+615
-19
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ Currently, with this SDK you can:
3030
- Inbox management
3131
- Project management
3232
- Contact management
33-
- Contacts CRUD
34-
- Lists CRUD
35-
- Contact Fields CRUD
33+
- Contacts
3634
- Contact Exports
35+
- Contact Fields
36+
- Contact Imports
37+
- Contact Lists
3738
- General
38-
- Templates CRUD
39-
- Suppressions management (find and delete)
39+
- Templates
40+
- Suppressions management
4041
- Account access management
4142
- Permissions management
4243
- List accounts you have access to
@@ -130,6 +131,9 @@ Refer to the [`examples`](examples) folder for the source code of this and other
130131
### Contact Exports API
131132

132133
- [Contact Exports](examples/contact-exports/everything.ts)
134+
### Contact Imports API
135+
136+
- [Contact Imports](examples/contact-imports/everything.ts)
133137

134138
### Sending API
135139

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+
email: "customer1@example.com"
16+
},
17+
{
18+
email: "customer2@example.com"
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();

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"axios": ">=0.27"
88
},
99
"devDependencies": {
10+
"rimraf": "^5.0.5",
1011
"@babel/core": "^7.20.5",
1112
"@babel/preset-env": "^7.20.2",
1213
"@babel/preset-typescript": "^7.18.6",
@@ -39,6 +40,13 @@
3940
],
4041
"license": "MIT",
4142
"main": "dist/index.js",
43+
"exports": {
44+
".": {
45+
"require": "./dist/index.js",
46+
"types": "./dist/index.d.ts"
47+
},
48+
"./package.json": "./package.json"
49+
},
4250
"peerDependencies": {
4351
"@types/nodemailer": "^6.4.9",
4452
"nodemailer": "^7.0.7"
@@ -53,10 +61,12 @@
5361
},
5462
"repository": "https://github.com/railsware/mailtrap-nodejs",
5563
"scripts": {
64+
"build": "rimraf dist && tsc --project tsconfig.build.json",
5665
"lint": "yarn lint:eslint && yarn lint:tsc",
5766
"lint:eslint": "yarn run eslint . --ext .js,.ts",
5867
"lint:tsc": "tsc -p . --noEmit --incremental false",
59-
"prepublish": "rm -rf dist && tsc --project tsconfig.build.json",
68+
"prepare": "yarn build",
69+
"prepublishOnly": "yarn build",
6070
"test": "jest --verbose",
6171
"coverage": "jest --coverage"
6272
},
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+
email: "customer1@example.com",
26+
fields: {
27+
first_name: "John",
28+
last_name: "Doe",
29+
},
30+
list_ids_included: [1, 2],
31+
},
32+
{
33+
email: "customer2@example.com",
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+
email: "customer@example.com",
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+
});

0 commit comments

Comments
 (0)