From d45d2c243ffb5f272289129735fbf711a4023cad Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Thu, 5 Nov 2026 10:41:34 +0200 Subject: [PATCH 1/4] List accounts through client.general without an accountId GET /api/accounts does not need an account ID, but the client.general getter required one. So getAllAccounts() failed with "accountId is missing" on a client that had no accountId. That is the case when you list accounts to find your account ID. - Build GeneralAPI with the optional accountId. GeneralAPI already checks the ID in the account-scoped resources. - Throw MailtrapError(ACCOUNT_ID_MISSING) from those resources, the same error as the other client getters. The old message told client users to create a GeneralAPI instance. - Remove accountId from the accounts example. Co-Authored-By: Claude Opus 5.5 --- examples/general/accounts.ts | 5 ++--- src/__tests__/lib/api/General.test.ts | 21 ++++++++---------- src/__tests__/lib/mailtrap-client.test.ts | 26 ++++++++++++++++------- src/lib/MailtrapClient.ts | 5 ++--- src/lib/api/General.ts | 8 ++++--- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/examples/general/accounts.ts b/examples/general/accounts.ts index 73b795f2..9c73c551 100644 --- a/examples/general/accounts.ts +++ b/examples/general/accounts.ts @@ -1,10 +1,9 @@ import { MailtrapClient } from "mailtrap" const TOKEN = ""; -const TEST_INBOX_ID = "" -const ACCOUNT_ID = "" -const client = new MailtrapClient({ token: TOKEN, testInboxId: TEST_INBOX_ID, accountId: ACCOUNT_ID }); +// Listing accounts does not need an accountId, so you can use it to find yours. +const client = new MailtrapClient({ token: TOKEN }); const accountsClient = client.general.accounts diff --git a/src/__tests__/lib/api/General.test.ts b/src/__tests__/lib/api/General.test.ts index 51928e23..7ee6eccf 100644 --- a/src/__tests__/lib/api/General.test.ts +++ b/src/__tests__/lib/api/General.test.ts @@ -1,6 +1,11 @@ import axios from "axios"; import General from "../../../lib/api/General"; +import MailtrapError from "../../../lib/MailtrapError"; + +import CONFIG from "../../../config"; + +const { ACCOUNT_ID_MISSING } = CONFIG.ERRORS; describe("lib/api/General: ", () => { const testAccountId = 100; @@ -47,36 +52,28 @@ describe("lib/api/General: ", () => { expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions generalAPI.accountAccesses; - }).toThrow( - "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." - ); + }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); }); it("throws error when accessing permissions without accountId.", () => { expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions generalAPI.permissions; - }).toThrow( - "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." - ); + }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); }); it("throws error when accessing billing without accountId.", () => { expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions generalAPI.billing; - }).toThrow( - "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." - ); + }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); }); it("throws error when accessing apiTokens without accountId.", () => { expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-expressions generalAPI.apiTokens; - }).toThrow( - "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." - ); + }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); }); }); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index c972d3f1..b51feef9 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -21,7 +21,8 @@ import ContactEventsBaseAPI from "../../lib/api/ContactEvents"; import MarketingStatsBaseAPI from "../../lib/api/MarketingStats"; const { ERRORS, CLIENT_SETTINGS } = CONFIG; -const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT } = CLIENT_SETTINGS; +const { TESTING_ENDPOINT, BULK_ENDPOINT, SENDING_ENDPOINT, GENERAL_ENDPOINT } = + CLIENT_SETTINGS; const { ACCOUNT_ID_MISSING, BULK_SANDBOX_INCOMPATIBLE, TEST_INBOX_ID_MISSING } = ERRORS; @@ -751,17 +752,26 @@ describe("lib/mailtrap-client: ", () => { }); describe("get general(): ", () => { - it("rejects with Mailtrap error, when `accountId` is missing.", () => { + it("returns general API object with accounts, but rejects account-scoped resources, when `accountId` is missing.", async () => { + const accountsMock = new AxiosMockAdapter(axios); + const accounts = [{ id: 1, name: "Account", access_levels: [1000] }]; + accountsMock + .onGet(`${GENERAL_ENDPOINT}/api/accounts`) + .reply(200, accounts); + const client = new MailtrapClient({ token: "MY_API_TOKEN", }); - expect.assertions(1); - try { - client.general; - } catch (error) { - expect(error).toEqual(new MailtrapError(ACCOUNT_ID_MISSING)); - } + expect(client.general).toBeInstanceOf(GeneralAPI); + await expect(client.general.accounts.getAllAccounts()).resolves.toEqual( + accounts + ); + expect(() => client.general.billing).toThrow( + new MailtrapError(ACCOUNT_ID_MISSING) + ); + + accountsMock.restore(); }); it("returns general API object when accountId is provided.", () => { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 9638a5a9..93d8a472 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -146,11 +146,10 @@ export default class MailtrapClient { } /** - * Getter for General API. + * Getter for General API. `accounts` works without accountId; other resources require it. */ get general() { - const accountId = this.validateAccountIdPresence(); - return new GeneralAPI(this.axios, accountId); + return new GeneralAPI(this.axios, this.accountId); } /** diff --git a/src/lib/api/General.ts b/src/lib/api/General.ts index 0a20e071..38fa9c08 100644 --- a/src/lib/api/General.ts +++ b/src/lib/api/General.ts @@ -1,11 +1,15 @@ import { AxiosInstance } from "axios"; +import CONFIG from "../../config"; +import MailtrapError from "../MailtrapError"; import AccountAccessesApi from "./resources/AccountAccesses"; import AccountsApi from "./resources/Accounts"; import ApiTokensApi from "./resources/ApiTokens"; import BillingApi from "./resources/Billing"; import PermissionsApi from "./resources/Permissions"; +const { ACCOUNT_ID_MISSING } = CONFIG.ERRORS; + export default class GeneralAPI { public accounts: AccountsApi; @@ -41,9 +45,7 @@ export default class GeneralAPI { */ private checkAccountIdPresence(): number { if (this.accountId === null) { - throw new Error( - "Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance." - ); + throw new MailtrapError(ACCOUNT_ID_MISSING); } return this.accountId; } From f809fe7c7dbb649716010fcc8ea366e9528bdc5d Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Thu, 5 Nov 2026 10:51:53 +0200 Subject: [PATCH 2/4] Assert MailtrapError class in missing-accountId tests Jest compares only the message when toThrow gets an error instance. Check the class and the message separately. Also check that billing works when accountId is set. Co-Authored-By: Claude Opus 5.5 --- src/__tests__/lib/api/General.test.ts | 24 ++++++++--------------- src/__tests__/lib/mailtrap-client.test.ts | 8 ++++---- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/__tests__/lib/api/General.test.ts b/src/__tests__/lib/api/General.test.ts index 7ee6eccf..53c1db1f 100644 --- a/src/__tests__/lib/api/General.test.ts +++ b/src/__tests__/lib/api/General.test.ts @@ -49,31 +49,23 @@ describe("lib/api/General: ", () => { const generalAPI = new General(axios); it("throws error when accessing accountAccesses without accountId.", () => { - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - generalAPI.accountAccesses; - }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); + expect(() => generalAPI.accountAccesses).toThrow(MailtrapError); + expect(() => generalAPI.accountAccesses).toThrow(ACCOUNT_ID_MISSING); }); it("throws error when accessing permissions without accountId.", () => { - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - generalAPI.permissions; - }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); + expect(() => generalAPI.permissions).toThrow(MailtrapError); + expect(() => generalAPI.permissions).toThrow(ACCOUNT_ID_MISSING); }); it("throws error when accessing billing without accountId.", () => { - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - generalAPI.billing; - }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); + expect(() => generalAPI.billing).toThrow(MailtrapError); + expect(() => generalAPI.billing).toThrow(ACCOUNT_ID_MISSING); }); it("throws error when accessing apiTokens without accountId.", () => { - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - generalAPI.apiTokens; - }).toThrow(new MailtrapError(ACCOUNT_ID_MISSING)); + expect(() => generalAPI.apiTokens).toThrow(MailtrapError); + expect(() => generalAPI.apiTokens).toThrow(ACCOUNT_ID_MISSING); }); }); diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index b51feef9..a7c9755b 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -767,9 +767,8 @@ describe("lib/mailtrap-client: ", () => { await expect(client.general.accounts.getAllAccounts()).resolves.toEqual( accounts ); - expect(() => client.general.billing).toThrow( - new MailtrapError(ACCOUNT_ID_MISSING) - ); + expect(() => client.general.billing).toThrow(MailtrapError); + expect(() => client.general.billing).toThrow(ACCOUNT_ID_MISSING); accountsMock.restore(); }); @@ -779,10 +778,11 @@ describe("lib/mailtrap-client: ", () => { token: "MY_API_TOKEN", accountId: 10, }); - expect.assertions(1); + expect.assertions(2); const generalClient = client.general; expect(generalClient).toBeInstanceOf(GeneralAPI); + expect(() => generalClient.billing).not.toThrow(); }); }); From bcfa84fb19e047c44112c3ffba455ce67426929e Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Thu, 5 Nov 2026 14:25:00 +0200 Subject: [PATCH 3/4] Treat accountId 0 as missing in client.general Use the same falsy check as the other getters. Restore the accounts mock in a finally block so a failed assertion does not leak it. Co-Authored-By: Claude Opus 5.5 --- src/__tests__/lib/mailtrap-client.test.ts | 28 +++++++++++++++-------- src/lib/MailtrapClient.ts | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index a7c9755b..0bcf4e12 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -759,18 +759,28 @@ describe("lib/mailtrap-client: ", () => { .onGet(`${GENERAL_ENDPOINT}/api/accounts`) .reply(200, accounts); - const client = new MailtrapClient({ + try { + const { general } = new MailtrapClient({ token: "MY_API_TOKEN" }); + + expect(general).toBeInstanceOf(GeneralAPI); + await expect(general.accounts.getAllAccounts()).resolves.toEqual( + accounts + ); + expect(() => general.billing).toThrow(MailtrapError); + expect(() => general.billing).toThrow(ACCOUNT_ID_MISSING); + } finally { + accountsMock.restore(); + } + }); + + it("rejects account-scoped resources, when `accountId` is 0.", () => { + const { general } = new MailtrapClient({ token: "MY_API_TOKEN", + accountId: 0, }); - expect(client.general).toBeInstanceOf(GeneralAPI); - await expect(client.general.accounts.getAllAccounts()).resolves.toEqual( - accounts - ); - expect(() => client.general.billing).toThrow(MailtrapError); - expect(() => client.general.billing).toThrow(ACCOUNT_ID_MISSING); - - accountsMock.restore(); + expect(() => general.billing).toThrow(MailtrapError); + expect(() => general.billing).toThrow(ACCOUNT_ID_MISSING); }); it("returns general API object when accountId is provided.", () => { diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 93d8a472..5a96750a 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -149,7 +149,7 @@ export default class MailtrapClient { * Getter for General API. `accounts` works without accountId; other resources require it. */ get general() { - return new GeneralAPI(this.axios, this.accountId); + return new GeneralAPI(this.axios, this.accountId || undefined); } /** From 5cd83291de0e5f213b09bfcd41fbb0c1246ded75 Mon Sep 17 00:00:00 2001 From: IzikAJ Date: Fri, 6 Nov 2026 13:57:06 +0200 Subject: [PATCH 4/4] Assert the billing URL for client.general with an accountId The old test only checked that general.billing did not throw. A wrong accountId in the wiring still passed. The test now calls the billing endpoint and expects /api/accounts/10/billing/usage. Co-Authored-By: Claude Opus 5.5 --- src/__tests__/lib/mailtrap-client.test.ts | 28 +++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/__tests__/lib/mailtrap-client.test.ts b/src/__tests__/lib/mailtrap-client.test.ts index 0bcf4e12..c3de9f76 100644 --- a/src/__tests__/lib/mailtrap-client.test.ts +++ b/src/__tests__/lib/mailtrap-client.test.ts @@ -783,16 +783,26 @@ describe("lib/mailtrap-client: ", () => { expect(() => general.billing).toThrow(ACCOUNT_ID_MISSING); }); - it("returns general API object when accountId is provided.", () => { - const client = new MailtrapClient({ - token: "MY_API_TOKEN", - accountId: 10, - }); - expect.assertions(2); + it("returns general API object scoped to the provided accountId.", async () => { + const billingMock = new AxiosMockAdapter(axios); + const usage = { billing: { cycle_start: "2026-11-01" } }; + billingMock + .onGet(`${GENERAL_ENDPOINT}/api/accounts/10/billing/usage`) + .reply(200, usage); - const generalClient = client.general; - expect(generalClient).toBeInstanceOf(GeneralAPI); - expect(() => generalClient.billing).not.toThrow(); + try { + const { general } = new MailtrapClient({ + token: "MY_API_TOKEN", + accountId: 10, + }); + + expect(general).toBeInstanceOf(GeneralAPI); + await expect( + general.billing.getCurrentBillingCycleUsage() + ).resolves.toEqual(usage); + } finally { + billingMock.restore(); + } }); });