Skip to content

Commit 596db0b

Browse files
Merge main into update-readme: resolve conflicts in README.md
2 parents 523d485 + 6bd88de commit 596db0b

File tree

11 files changed

+1132
-49
lines changed

11 files changed

+1132
-49
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ General API:
280280

281281
- Suppressions (find & delete) – [`sending/suppressions.ts`](examples/sending/suppressions.ts)
282282

283-
- Billing info – (no example yet) ← add in future
283+
- Billing info – [`general/billing.ts`](examples/general/billing.ts)
284284

285285
- Accounts info – [`general/accounts.ts`](examples/general/accounts.ts)
286286

examples/general/billing.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { MailtrapClient } from "mailtrap"
2+
3+
const TOKEN = "<YOUR-TOKEN-HERE>";
4+
const TEST_INBOX_ID = "<YOUR-TEST-INBOX-ID-HERE>"
5+
const ACCOUNT_ID = "<YOUR-ACCOUNT-ID-HERE>"
6+
7+
const client = new MailtrapClient({ token: TOKEN, testInboxId: TEST_INBOX_ID, accountId: ACCOUNT_ID });
8+
9+
const billingClient = client.general.billing
10+
11+
const testBillingCycleUsage = async () => {
12+
try {
13+
const result = await billingClient.getCurrentBillingCycleUsage()
14+
console.log("Billing cycle usage:", JSON.stringify(result, null, 2))
15+
} catch (error) {
16+
console.error(error)
17+
}
18+
}
19+
20+
testBillingCycleUsage()

src/__tests__/lib/api/General.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ describe("lib/api/General: ", () => {
1313
expect(generalAPI).toHaveProperty("accountAccesses");
1414
expect(generalAPI).toHaveProperty("accounts");
1515
expect(generalAPI).toHaveProperty("permissions");
16+
expect(generalAPI).toHaveProperty("billing");
1617
});
1718

1819
it("lazily instantiates account-specific APIs via getters when accountId is provided.", () => {
1920
expect(generalAPI.accountAccesses).toBeDefined();
2021
expect(generalAPI.permissions).toBeDefined();
22+
expect(generalAPI.billing).toBeDefined();
2123
expect(generalAPI.accounts).toBeDefined();
2224
});
2325
});
@@ -56,6 +58,15 @@ describe("lib/api/General: ", () => {
5658
"Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance."
5759
);
5860
});
61+
62+
it("throws error when accessing billing without accountId.", () => {
63+
expect(() => {
64+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
65+
generalAPI.billing;
66+
}).toThrow(
67+
"Account ID is required for this operation. Please provide accountId when creating GeneralAPI instance."
68+
);
69+
});
5970
});
6071

6172
describe("account discovery functionality: ", () => {
@@ -78,10 +89,14 @@ describe("lib/api/General: ", () => {
7889
it("maintains existing API surface for account-specific operations.", () => {
7990
expect(generalAPI.accountAccesses).toBeDefined();
8091
expect(generalAPI.permissions).toBeDefined();
92+
expect(generalAPI.billing).toBeDefined();
8193
expect(typeof generalAPI.accountAccesses.listAccountAccesses).toBe(
8294
"function"
8395
);
8496
expect(typeof generalAPI.permissions.getResources).toBe("function");
97+
expect(typeof generalAPI.billing.getCurrentBillingCycleUsage).toBe(
98+
"function"
99+
);
85100
});
86101
});
87102

@@ -109,10 +124,14 @@ describe("lib/api/General: ", () => {
109124
expect(generalAPI.accounts).toBeDefined();
110125
expect(generalAPI.accountAccesses).toBeDefined();
111126
expect(generalAPI.permissions).toBeDefined();
127+
expect(generalAPI.billing).toBeDefined();
112128
expect(typeof generalAPI.accountAccesses.listAccountAccesses).toBe(
113129
"function"
114130
);
115131
expect(typeof generalAPI.permissions.getResources).toBe("function");
132+
expect(typeof generalAPI.billing.getCurrentBillingCycleUsage).toBe(
133+
"function"
134+
);
116135
});
117136
});
118137
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import axios from "axios";
2+
import AxiosMockAdapter from "axios-mock-adapter";
3+
4+
import Billing from "../../../../lib/api/resources/Billing";
5+
import handleSendingError from "../../../../lib/axios-logger";
6+
import MailtrapError from "../../../../lib/MailtrapError";
7+
8+
import CONFIG from "../../../../config";
9+
10+
const { CLIENT_SETTINGS } = CONFIG;
11+
const { GENERAL_ENDPOINT } = CLIENT_SETTINGS;
12+
13+
describe("lib/api/resources/Billing: ", () => {
14+
let mock: AxiosMockAdapter;
15+
const accountId = 100;
16+
const billingAPI = new Billing(axios, accountId);
17+
const responseData = {
18+
billing: {
19+
cycle_start: "2024-01-01T00:00:00Z",
20+
cycle_end: "2024-01-31T23:59:59Z",
21+
},
22+
sending: {
23+
plan: {
24+
name: "Pro",
25+
},
26+
usage: {
27+
sent_messages_count: {
28+
current: 1000,
29+
limit: 10000,
30+
},
31+
},
32+
},
33+
testing: {
34+
plan: {
35+
name: "Pro",
36+
},
37+
usage: {
38+
sent_messages_count: {
39+
current: 500,
40+
limit: 5000,
41+
},
42+
forwarded_messages_count: {
43+
current: 200,
44+
limit: 2000,
45+
},
46+
},
47+
},
48+
};
49+
50+
describe("class Billing(): ", () => {
51+
describe("init: ", () => {
52+
it("initializes with all necessary params.", () => {
53+
expect(billingAPI).toHaveProperty("getCurrentBillingCycleUsage");
54+
});
55+
});
56+
});
57+
58+
beforeAll(() => {
59+
/**
60+
* Init Axios interceptors for handling response.data, errors.
61+
*/
62+
axios.interceptors.response.use(
63+
(response) => response.data,
64+
handleSendingError
65+
);
66+
mock = new AxiosMockAdapter(axios);
67+
});
68+
69+
afterEach(() => {
70+
mock.reset();
71+
});
72+
73+
describe("getCurrentBillingCycleUsage(): ", () => {
74+
it("successfully gets billing cycle usage.", async () => {
75+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/billing/usage`;
76+
77+
expect.assertions(2);
78+
79+
mock.onGet(endpoint).reply(200, responseData);
80+
const result = await billingAPI.getCurrentBillingCycleUsage();
81+
82+
expect(mock.history.get[0].url).toEqual(endpoint);
83+
expect(result).toEqual(responseData);
84+
});
85+
86+
it("fails with error when accountId is invalid.", async () => {
87+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/billing/usage`;
88+
const expectedErrorMessage = "Account not found";
89+
const statusCode = 404;
90+
91+
expect.assertions(3);
92+
93+
mock.onGet(endpoint).reply(statusCode, { error: expectedErrorMessage });
94+
95+
try {
96+
await billingAPI.getCurrentBillingCycleUsage();
97+
} catch (error) {
98+
expect(error).toBeInstanceOf(MailtrapError);
99+
100+
if (error instanceof MailtrapError) {
101+
expect(error.message).toEqual(expectedErrorMessage);
102+
// Verify status code is accessible via cause property
103+
// @ts-expect-error ES5 types don't know about cause property
104+
expect(error.cause?.response?.status).toEqual(statusCode);
105+
}
106+
}
107+
});
108+
109+
it("fails with error when billing endpoint returns 403 Forbidden.", async () => {
110+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/billing/usage`;
111+
const expectedErrorMessage = "Access denied";
112+
const statusCode = 403;
113+
114+
expect.assertions(3);
115+
116+
mock.onGet(endpoint).reply(statusCode, { error: expectedErrorMessage });
117+
118+
try {
119+
await billingAPI.getCurrentBillingCycleUsage();
120+
} catch (error) {
121+
expect(error).toBeInstanceOf(MailtrapError);
122+
123+
if (error instanceof MailtrapError) {
124+
expect(error.message).toEqual(expectedErrorMessage);
125+
// Verify status code is accessible via cause property
126+
// @ts-expect-error ES5 types don't know about cause property
127+
expect(error.cause?.response?.status).toEqual(statusCode);
128+
}
129+
}
130+
});
131+
132+
it("fails with error when no error body is provided.", async () => {
133+
const endpoint = `${GENERAL_ENDPOINT}/api/accounts/${accountId}/billing/usage`;
134+
const expectedErrorMessage = "Request failed with status code 500";
135+
const statusCode = 500;
136+
137+
expect.assertions(3);
138+
139+
mock.onGet(endpoint).reply(statusCode);
140+
141+
try {
142+
await billingAPI.getCurrentBillingCycleUsage();
143+
} catch (error) {
144+
expect(error).toBeInstanceOf(MailtrapError);
145+
146+
if (error instanceof MailtrapError) {
147+
expect(error.message).toEqual(expectedErrorMessage);
148+
// Verify status code is accessible via cause property
149+
// @ts-expect-error ES5 types don't know about cause property
150+
expect(error.cause?.response?.status).toEqual(statusCode);
151+
}
152+
}
153+
});
154+
});
155+
});

src/__tests__/lib/api/resources/ContactExports.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ describe("lib/api/resources/ContactExports: ", () => {
111111
} catch (error) {
112112
expect(error).toBeInstanceOf(MailtrapError);
113113
if (error instanceof MailtrapError) {
114-
// axios logger returns "[object Object]" for error objects, so we check for that
115-
expect(error.message).toBe("[object Object]");
114+
// When errors object doesn't match recognized pattern, falls back to default Axios error message
115+
expect(error.message).toBe("Request failed with status code 422");
116116
}
117117
}
118118
});

src/__tests__/lib/api/resources/ContactImports.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,9 @@ describe("lib/api/resources/ContactImports: ", () => {
198198
} catch (error) {
199199
expect(error).toBeInstanceOf(MailtrapError);
200200
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]");
201+
expect(error.message).toBe(
202+
"invalid-email-1: email: is invalid, is required | invalid-email-2: Contact limit exceeded"
203+
);
206204
}
207205
}
208206
});

0 commit comments

Comments
 (0)