-
Notifications
You must be signed in to change notification settings - Fork 13
Contact imports #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Contact imports #95
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e91925e
feat: add Contact Imports API and refactor account ID validation in M…
narekhovhannisyan 3096ecc
feat: implement ContactImportsBaseAPI for managing contact imports
narekhovhannisyan 1844b31
feat: add ContactImportsApi for managing contact imports with get and…
narekhovhannisyan 068ade5
feat: add types for ContactImportResponse and ImportContactsRequest t…
narekhovhannisyan 2417839
examples: add example for contact import flow using MailtrapClient
narekhovhannisyan 1b96cca
refactor: enhance error handling in axios logger with detailed format…
narekhovhannisyan bff0cb1
refactor: simplify error handling in axios logger by consolidating er…
narekhovhannisyan 92adcc8
docs: update README to include Contact Imports feature and examples
narekhovhannisyan 1622a36
test: add unit tests for ContactImports class to verify initializatio…
narekhovhannisyan 1ad0109
test: add comprehensive unit tests for ContactImportsApi class, cover…
narekhovhannisyan 0cc415c
test: update ContactImports tests to reflect new API error response f…
narekhovhannisyan 560cd60
chore: empty commit to retry CI/CD
narekhovhannisyan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.