From e91925ec7527f696aa9f27f464e75cf7497b1f2a Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 27 Oct 2025 14:23:59 +0400 Subject: [PATCH 01/12] feat: add Contact Imports API and refactor account ID validation in MailtrapClient --- src/lib/MailtrapClient.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index baa6bb2..c562ece 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -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"; @@ -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. */ @@ -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); } /** From 3096ecc9199f42ad05760b1c78d4e144e1e0a955 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 27 Oct 2025 14:24:11 +0400 Subject: [PATCH 02/12] feat: implement ContactImportsBaseAPI for managing contact imports --- src/lib/api/ContactImports.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/lib/api/ContactImports.ts diff --git a/src/lib/api/ContactImports.ts b/src/lib/api/ContactImports.ts new file mode 100644 index 0000000..f83c68c --- /dev/null +++ b/src/lib/api/ContactImports.ts @@ -0,0 +1,15 @@ +import { AxiosInstance } from "axios"; + +import ContactImportsApi from "./resources/ContactImports"; + +export default class ContactImportsBaseAPI { + public create: ContactImportsApi["create"]; + + public get: ContactImportsApi["get"]; + + constructor(client: AxiosInstance, accountId: number) { + const contactImports = new ContactImportsApi(client, accountId); + this.create = contactImports.create.bind(contactImports); + this.get = contactImports.get.bind(contactImports); + } +} From 1844b31958caad3af6f88217acc8b539cb8d9cd2 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 27 Oct 2025 14:24:18 +0400 Subject: [PATCH 03/12] feat: add ContactImportsApi for managing contact imports with get and create methods --- src/lib/api/resources/ContactImports.ts | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/lib/api/resources/ContactImports.ts diff --git a/src/lib/api/resources/ContactImports.ts b/src/lib/api/resources/ContactImports.ts new file mode 100644 index 0000000..a007ee0 --- /dev/null +++ b/src/lib/api/resources/ContactImports.ts @@ -0,0 +1,43 @@ +import { AxiosInstance } from "axios"; + +import CONFIG from "../../../config"; + +import { + ContactImportResponse, + ImportContactsRequest, +} from "../../../types/api/contact-imports"; + +const { CLIENT_SETTINGS } = CONFIG; +const { GENERAL_ENDPOINT } = CLIENT_SETTINGS; + +export default class ContactImportsApi { + private client: AxiosInstance; + + private contactImportsURL: string; + + constructor(client: AxiosInstance, accountId: number) { + this.client = client; + this.contactImportsURL = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/contacts/imports`; + } + + /** + * Get a contact import by `importId`. + */ + public async get(importId: number) { + const url = `${this.contactImportsURL}/${importId}`; + + return this.client.get(url); + } + + /** + * Import contacts. + */ + public async create(data: ImportContactsRequest) { + const url = `${this.contactImportsURL}`; + + return this.client.post( + url, + data + ); + } +} From 068ade5ab97f55c9753654d294b27beb27f5ea56 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 27 Oct 2025 14:24:27 +0400 Subject: [PATCH 04/12] feat: add types for ContactImportResponse and ImportContactsRequest to support contact import functionality --- src/types/api/contact-imports.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/types/api/contact-imports.ts diff --git a/src/types/api/contact-imports.ts b/src/types/api/contact-imports.ts new file mode 100644 index 0000000..86b624f --- /dev/null +++ b/src/types/api/contact-imports.ts @@ -0,0 +1,21 @@ +export type ContactImportResponse = { + id: number; + status: "created" | "started" | "finished" | "failed"; + created_contacts_count?: number; + updated_contacts_count?: number; + contacts_over_limit_count?: number; +}; + +export type ImportContactsRequest = { + contacts: { + email: string; + fields?: { + first_name?: string; + last_name?: string; + zip_code?: number; + [key: string]: string | number | undefined; + }; + list_ids_included?: number[]; + list_ids_excluded?: number[]; + }[]; +}; From 24178390d8fc34a35440216f3b41d244ab2105b6 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Mon, 27 Oct 2025 14:28:42 +0400 Subject: [PATCH 05/12] examples: add example for contact import flow using MailtrapClient --- examples/contact-imports/everything.ts | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/contact-imports/everything.ts diff --git a/examples/contact-imports/everything.ts b/examples/contact-imports/everything.ts new file mode 100644 index 0000000..c1aecf4 --- /dev/null +++ b/examples/contact-imports/everything.ts @@ -0,0 +1,36 @@ +import { MailtrapClient } from "mailtrap"; + +const TOKEN = ""; +const ACCOUNT_ID = ""; + +const client = new MailtrapClient({ + token: TOKEN, + accountId: ACCOUNT_ID +}); + +async function runContactImportsFlow() { + const importData = { + contacts: [ + { + email: "customer1@example.com" + }, + { + email: "customer2@example.com" + } + ] + }; + + 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(); From 1b96cca69863384ce49f785541e6f3559eb2fd5d Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 28 Oct 2025 14:50:35 +0400 Subject: [PATCH 06/12] refactor: enhance error handling in axios logger with detailed formatting for API error messages --- src/lib/axios-logger.ts | 161 ++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 41 deletions(-) diff --git a/src/lib/axios-logger.ts b/src/lib/axios-logger.ts index 478f048..60f7af9 100644 --- a/src/lib/axios-logger.ts +++ b/src/lib/axios-logger.ts @@ -14,52 +14,131 @@ const hasErrorProperty = ( return (obj as AxiosErrorObject)?.[propertyName] !== undefined; }; +/** + * Formats a single error field into a readable string + */ +function formatErrorField([key, value]: [string, any]): string { + return Array.isArray(value) + ? `${key}: ${value.join(", ")}` + : `${key}: ${value}`; +} + +/** + * Formats validation errors for a single item (e.g., contact import error) + */ +function formatValidationItem(item: any): string { + const parts = []; + if (item.email) parts.push(`Email: ${item.email}`); + + if (item.errors) { + const errorParts = Object.entries(item.errors).map(formatErrorField); + if (errorParts.length > 0) { + parts.push(`Errors: ${errorParts.join("; ")}`); + } + } + + return parts.join(" - "); +} + +/** + * Formats an array of validation errors + */ +function formatValidationErrors(errors: any[]): string { + return errors + .map((item) => + typeof item === "object" && item !== null + ? formatValidationItem(item) + : String(item) + ) + .join(" | "); +} + +/** + * Formats error messages from API response data + */ +function formatErrorMessage(data: any): string { + if (!data || typeof data !== "object") { + return String(data); + } + + // Handle array of errors + if (Array.isArray(data)) { + return formatValidationErrors(data); + } + + // Handle errors object with array of validation errors + if (data.errors && Array.isArray(data.errors)) { + return formatValidationErrors(data.errors); + } + + // Handle errors object with name/base arrays + if (data.errors && !Array.isArray(data.errors)) { + const errorParts = []; + if ( + hasErrorProperty(data.errors, "name") && + Array.isArray(data.errors.name) + ) { + errorParts.push(`Name errors: ${data.errors.name.join(", ")}`); + } + if ( + hasErrorProperty(data.errors, "base") && + Array.isArray(data.errors.base) + ) { + errorParts.push(`Base errors: ${data.errors.base.join(", ")}`); + } + if (errorParts.length > 0) { + return errorParts.join("; "); + } + } + + // Handle simple error message + if (data.error) { + return String(data.error); + } + + // Handle message field + if (data.message) { + return String(data.message); + } + + // Fallback to JSON string for complex objects + try { + return JSON.stringify(data, null, 2); + } catch { + return String(data); + } +} + /** * Error handler for axios response. */ export default function handleSendingError(error: AxiosError | unknown) { if (axios.isAxiosError(error)) { - /** - * Handles case where error is in `data.errors`. - */ - const serverErrorsObject = - error.response?.data && - typeof error.response.data === "object" && - "errors" in error.response.data && - error.response.data.errors; - - /** - * Handles case where error is in `data.error`. - */ - const serverErrorObject = - error.response?.data && - typeof error.response.data === "object" && - "error" in error.response.data && - error.response.data.error; - - /** - * Joins error messages contained in `name` property. - */ - const errorNames = - hasErrorProperty(serverErrorsObject, "name") && - serverErrorsObject.name.join(", "); - - /** - * Joins error messages contained in `base` property. - */ - const errorBase = - hasErrorProperty(serverErrorsObject, "base") && - serverErrorsObject.base.join(", "); - - /** - * Selects available error. - */ - const message = - errorNames || - errorBase || - serverErrorsObject || - serverErrorObject || - error.message; + const status = error.response?.status; + const statusText = error.response?.statusText; + const data = error.response?.data; + + let message = `Request failed with status ${status}`; + + // Add status text if available + if (statusText) { + message += ` (${statusText})`; + } + + // Add API error details if available + if (data) { + const formattedError = formatErrorMessage(data); + if (formattedError) { + message += `: ${formattedError}`; + } + } + + // Add URL for context + if (error.config?.url) { + message += ` | URL: ${error.config.method?.toUpperCase()} ${ + error.config.url + }`; + } // @ts-expect-error weird typing around Error class, but it's tested to work throw new MailtrapError(message, { cause: error }); From bff0cb1fa050a68ef2657953919ea14f96eb92aa Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 29 Oct 2025 18:10:28 +0400 Subject: [PATCH 07/12] refactor: simplify error handling in axios logger by consolidating error message formatting --- src/lib/axios-logger.ts | 161 ++++++++++------------------------------ 1 file changed, 41 insertions(+), 120 deletions(-) diff --git a/src/lib/axios-logger.ts b/src/lib/axios-logger.ts index 60f7af9..478f048 100644 --- a/src/lib/axios-logger.ts +++ b/src/lib/axios-logger.ts @@ -14,131 +14,52 @@ const hasErrorProperty = ( return (obj as AxiosErrorObject)?.[propertyName] !== undefined; }; -/** - * Formats a single error field into a readable string - */ -function formatErrorField([key, value]: [string, any]): string { - return Array.isArray(value) - ? `${key}: ${value.join(", ")}` - : `${key}: ${value}`; -} - -/** - * Formats validation errors for a single item (e.g., contact import error) - */ -function formatValidationItem(item: any): string { - const parts = []; - if (item.email) parts.push(`Email: ${item.email}`); - - if (item.errors) { - const errorParts = Object.entries(item.errors).map(formatErrorField); - if (errorParts.length > 0) { - parts.push(`Errors: ${errorParts.join("; ")}`); - } - } - - return parts.join(" - "); -} - -/** - * Formats an array of validation errors - */ -function formatValidationErrors(errors: any[]): string { - return errors - .map((item) => - typeof item === "object" && item !== null - ? formatValidationItem(item) - : String(item) - ) - .join(" | "); -} - -/** - * Formats error messages from API response data - */ -function formatErrorMessage(data: any): string { - if (!data || typeof data !== "object") { - return String(data); - } - - // Handle array of errors - if (Array.isArray(data)) { - return formatValidationErrors(data); - } - - // Handle errors object with array of validation errors - if (data.errors && Array.isArray(data.errors)) { - return formatValidationErrors(data.errors); - } - - // Handle errors object with name/base arrays - if (data.errors && !Array.isArray(data.errors)) { - const errorParts = []; - if ( - hasErrorProperty(data.errors, "name") && - Array.isArray(data.errors.name) - ) { - errorParts.push(`Name errors: ${data.errors.name.join(", ")}`); - } - if ( - hasErrorProperty(data.errors, "base") && - Array.isArray(data.errors.base) - ) { - errorParts.push(`Base errors: ${data.errors.base.join(", ")}`); - } - if (errorParts.length > 0) { - return errorParts.join("; "); - } - } - - // Handle simple error message - if (data.error) { - return String(data.error); - } - - // Handle message field - if (data.message) { - return String(data.message); - } - - // Fallback to JSON string for complex objects - try { - return JSON.stringify(data, null, 2); - } catch { - return String(data); - } -} - /** * Error handler for axios response. */ export default function handleSendingError(error: AxiosError | unknown) { if (axios.isAxiosError(error)) { - const status = error.response?.status; - const statusText = error.response?.statusText; - const data = error.response?.data; - - let message = `Request failed with status ${status}`; - - // Add status text if available - if (statusText) { - message += ` (${statusText})`; - } - - // Add API error details if available - if (data) { - const formattedError = formatErrorMessage(data); - if (formattedError) { - message += `: ${formattedError}`; - } - } - - // Add URL for context - if (error.config?.url) { - message += ` | URL: ${error.config.method?.toUpperCase()} ${ - error.config.url - }`; - } + /** + * Handles case where error is in `data.errors`. + */ + const serverErrorsObject = + error.response?.data && + typeof error.response.data === "object" && + "errors" in error.response.data && + error.response.data.errors; + + /** + * Handles case where error is in `data.error`. + */ + const serverErrorObject = + error.response?.data && + typeof error.response.data === "object" && + "error" in error.response.data && + error.response.data.error; + + /** + * Joins error messages contained in `name` property. + */ + const errorNames = + hasErrorProperty(serverErrorsObject, "name") && + serverErrorsObject.name.join(", "); + + /** + * Joins error messages contained in `base` property. + */ + const errorBase = + hasErrorProperty(serverErrorsObject, "base") && + serverErrorsObject.base.join(", "); + + /** + * Selects available error. + */ + const message = + errorNames || + errorBase || + serverErrorsObject || + serverErrorObject || + error.message; // @ts-expect-error weird typing around Error class, but it's tested to work throw new MailtrapError(message, { cause: error }); From 92adcc800d116f4de21a1cc4422db918a3c20d4b Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 29 Oct 2025 18:14:16 +0400 Subject: [PATCH 08/12] docs: update README to include Contact Imports feature and examples --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index edecd5a..e7936e2 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) From 1622a368ee32fcf8885bcbb1a387b3dcd5cbc392 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 29 Oct 2025 18:14:25 +0400 Subject: [PATCH 09/12] test: add unit tests for ContactImports class to verify initialization and method properties --- src/__tests__/lib/api/ContactImports.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/__tests__/lib/api/ContactImports.test.ts diff --git a/src/__tests__/lib/api/ContactImports.test.ts b/src/__tests__/lib/api/ContactImports.test.ts new file mode 100644 index 0000000..26fd856 --- /dev/null +++ b/src/__tests__/lib/api/ContactImports.test.ts @@ -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"); + }); + }); + }); +}); From 1ad010960f58b268502f38533a9ee49a891930b1 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 29 Oct 2025 18:17:00 +0400 Subject: [PATCH 10/12] test: add comprehensive unit tests for ContactImportsApi class, covering create and get methods with various scenarios --- .../lib/api/resources/ContactImports.test.ts | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/__tests__/lib/api/resources/ContactImports.test.ts diff --git a/src/__tests__/lib/api/resources/ContactImports.test.ts b/src/__tests__/lib/api/resources/ContactImports.test.ts new file mode 100644 index 0000000..3c9c30b --- /dev/null +++ b/src/__tests__/lib/api/resources/ContactImports.test.ts @@ -0,0 +1,251 @@ +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: "customer1@example.com", + fields: { + first_name: "John", + last_name: "Doe", + }, + list_ids_included: [1, 2], + }, + { + email: "customer2@example.com", + 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: "customer@example.com", + }, + ], + }; + 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 in data.errors + mock.onPost(endpoint).reply(422, { + errors: { + name: ["is invalid", "is required"], + base: ["Contact limit exceeded"], + }, + }); + + try { + await contactImportsAPI.create(invalidRequest); + } catch (error) { + expect(error).toBeInstanceOf(MailtrapError); + if (error instanceof MailtrapError) { + // axios-logger joins name errors with ", " + expect(error.message).toBe("is invalid, is required"); + } + } + }); + }); + + 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); + } + } + }); + }); +}); From 0cc415c24d9cf525d2dc4027b1286150087dec2a Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 6 Nov 2025 16:15:04 +0400 Subject: [PATCH 11/12] test: update ContactImports tests to reflect new API error response format with detailed error objects --- .../lib/api/resources/ContactImports.test.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/__tests__/lib/api/resources/ContactImports.test.ts b/src/__tests__/lib/api/resources/ContactImports.test.ts index 3c9c30b..82a13ca 100644 --- a/src/__tests__/lib/api/resources/ContactImports.test.ts +++ b/src/__tests__/lib/api/resources/ContactImports.test.ts @@ -174,12 +174,23 @@ describe("lib/api/resources/ContactImports: ", () => { expect.assertions(2); - // API returns errors as an array in data.errors + // 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: { - name: ["is invalid", "is required"], - base: ["Contact limit exceeded"], - }, + errors: [ + { + email: "invalid-email-1", + errors: { + email: ["is invalid", "is required"], + }, + }, + { + email: "invalid-email-2", + errors: { + base: ["Contact limit exceeded"], + }, + }, + ], }); try { @@ -187,8 +198,11 @@ describe("lib/api/resources/ContactImports: ", () => { } catch (error) { expect(error).toBeInstanceOf(MailtrapError); if (error instanceof MailtrapError) { - // axios-logger joins name errors with ", " - expect(error.message).toBe("is invalid, is required"); + // 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]"); } } }); From 560cd60030bd6b82702cf2705b8b8eaf46398948 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Thu, 6 Nov 2025 16:24:09 +0400 Subject: [PATCH 12/12] chore: empty commit to retry CI/CD