diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9735b11..2bc95d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,17 @@ jobs: - run: pnpm typecheck - run: pnpm test - run: pnpm build + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4.3.0 + with: + version: 9 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..74f85d2 --- /dev/null +++ b/biome.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "suspicious": { + "noExplicitAny": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always" + } + } +} diff --git a/package.json b/package.json index b80ca74..a5e89ee 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,15 @@ "type": "git", "url": "git+https://github.com/MillionSend/millionsend-node.git" }, - "keywords": ["millionsend", "resend", "email", "ses", "sdk", "transactional", "broadcast"], + "keywords": [ + "millionsend", + "resend", + "email", + "ses", + "sdk", + "transactional", + "broadcast" + ], "exports": { ".": { "types": "./dist/index.d.ts", @@ -20,7 +28,12 @@ "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", - "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"], + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], "engines": { "node": ">=18" }, @@ -28,9 +41,12 @@ "build": "tsc -p tsconfig.build.json", "typecheck": "tsc --noEmit", "test": "vitest run", + "lint": "biome check .", + "lint:fix": "biome check --write .", "prepack": "pnpm build" }, "devDependencies": { + "@biomejs/biome": "2.5.13", "@types/node": "^22.20.1", "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2e6bf6..098b350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: 2.5.13 + version: 2.5.13 '@types/node': specifier: ^22.20.1 version: 22.20.1 @@ -20,6 +23,63 @@ importers: packages: + '@biomejs/biome@2.5.13': + resolution: {integrity: sha512-+SEC/mFk1a+5mvUANZgbZTaiZXs1nj4iMhL/PHiqDT5TPUEPFIlliEtkKKZB4N862yylHC3UI+/Sj2I0HJEqhA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.13': + resolution: {integrity: sha512-nYSuDJ6zgVqZUkAkJkvhXxsF2PYIrUk/g638K4voCXz7foI9f7b6C2/7+oAihsHQlivZNMUrm/y8ywlcHQtZOw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.13': + resolution: {integrity: sha512-KVy1ceEDuJ3AzFxjT9kkxbVy+UANw1pjEMUS6lvKfxjJ+fmkRvc7sQn1Xo6ETgseEI7wUQoV03KSga3XfE8YRQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.13': + resolution: {integrity: sha512-CH32xpep3dNS5EVJpAHlYchkBYznxyVZQrx0b6YYYlPL9u9ZeD2NtqiK/6s64+HFS2a7hGnryLlqqZAOh8ax5g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.13': + resolution: {integrity: sha512-VlNMtoxOqs0dUR6drxxHr18SNUvI7xxAuHZlH4s/dstYSf3g6RaqVgo8JRnhcOhKz9afWrvtJTboNG1JuYlIDQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.13': + resolution: {integrity: sha512-F3pmwl+VHoUuVJN/tbNLKeLt0SVK3EIyN1jXlMcNyQGYRQQTVqXF+GgkP0/CjGXFN87e/mv0sBfUnWqWza5PKQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.13': + resolution: {integrity: sha512-Fi6gIxbUaJ3ZCIXeG3ggIBPF76O2DjDN67WrPX/ODRv54GeXPm2gbEPC96Yb0yrJoiH0Eax1JLx1Pi0XbsNFZQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.13': + resolution: {integrity: sha512-+WD13qshXrr0Icv4BfsAdzm8Fs3TL+nZ59zEQxsYNd8lcgZJ0A5+OrlwsL1PipVCmWeRpxgIPD6DAcEd7smkCQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.13': + resolution: {integrity: sha512-VOofU/nW761XWzUeUNE8zzYNyPrxuMuNFMizL0qz7J75yeV8N7WFheUlk1/K6dOkaFpH9wJ+lDKlwjAbw0346w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -589,6 +649,41 @@ packages: snapshots: + '@biomejs/biome@2.5.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.13 + '@biomejs/cli-darwin-x64': 2.5.13 + '@biomejs/cli-linux-arm64': 2.5.13 + '@biomejs/cli-linux-arm64-musl': 2.5.13 + '@biomejs/cli-linux-x64': 2.5.13 + '@biomejs/cli-linux-x64-musl': 2.5.13 + '@biomejs/cli-win32-arm64': 2.5.13 + '@biomejs/cli-win32-x64': 2.5.13 + + '@biomejs/cli-darwin-arm64@2.5.13': + optional: true + + '@biomejs/cli-darwin-x64@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64@2.5.13': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-x64@2.5.13': + optional: true + + '@biomejs/cli-win32-arm64@2.5.13': + optional: true + + '@biomejs/cli-win32-x64@2.5.13': + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true diff --git a/src/error.ts b/src/error.ts index 2daeb30..a9b32a5 100644 --- a/src/error.ts +++ b/src/error.ts @@ -20,7 +20,8 @@ export function toErrorResponse(body: unknown, status: number): ErrorResponse { if (body && typeof body === "object") { const b = body as Record; const name = typeof b.name === "string" ? b.name : "application_error"; - const message = typeof b.message === "string" ? b.message : `Request failed with status ${status}`; + const message = + typeof b.message === "string" ? b.message : `Request failed with status ${status}`; const statusCode = typeof b.statusCode === "number" ? b.statusCode : status; return { name, message, statusCode }; } diff --git a/src/http.ts b/src/http.ts index 6e47491..704a42c 100644 --- a/src/http.ts +++ b/src/http.ts @@ -83,10 +83,11 @@ export class HttpClient { constructor(apiKey: string, options: MillionSendOptions = {}) { this.#apiKey = apiKey; - this.baseUrl = (options.baseUrl ?? process.env.MILLIONSEND_BASE_URL ?? DEFAULT_BASE_URL).replace( - /\/+$/, - "", - ); + this.baseUrl = ( + options.baseUrl ?? + process.env.MILLIONSEND_BASE_URL ?? + DEFAULT_BASE_URL + ).replace(/\/+$/, ""); if (!options.allowInsecureHttp && isInsecureHttpUrl(this.baseUrl)) { throw new Error( `Refusing to send the API key over plain http to ${this.baseUrl}. Use https, or set allowInsecureHttp: true.`, diff --git a/src/resources/contacts.ts b/src/resources/contacts.ts index 0eeb0b8..4142a04 100644 --- a/src/resources/contacts.ts +++ b/src/resources/contacts.ts @@ -17,8 +17,8 @@ import type { CreateContactOptions, CreateContactsBatchOptions, List, - ListContactTopicsOptions, ListContactsOptions, + ListContactTopicsOptions, RemoveContactOptions, RemoveContactResponse, RemoveContactSegmentResponse, @@ -162,7 +162,11 @@ export class Contacts { } update(options: UpdateContactOptions): Promise> { - return this.http.request({ method: "PATCH", path: contactPath(options), body: updateBody(options) }); + return this.http.request({ + method: "PATCH", + path: contactPath(options), + body: updateBody(options), + }); } /** DELETE /contacts/:id — `erase` also scrubs the address from email history (`?erase=true`). */ diff --git a/src/resources/emails.ts b/src/resources/emails.ts index 103f058..254590d 100644 --- a/src/resources/emails.ts +++ b/src/resources/emails.ts @@ -110,7 +110,10 @@ export class Batch { constructor(private readonly http: HttpClient) {} /** POST /emails/batch — 1–100 emails in one call; supports an Idempotency-Key and `batchValidation`. */ - send(payload: SendEmailOptions[], options: BatchRequestOptions = {}): Promise> { + send( + payload: SendEmailOptions[], + options: BatchRequestOptions = {}, + ): Promise> { return this.http.request({ method: "POST", path: "/emails/batch", @@ -121,7 +124,10 @@ export class Batch { } /** Alias of {@link send}, mirroring Resend. */ - create(payload: SendEmailOptions[], options: BatchRequestOptions = {}): Promise> { + create( + payload: SendEmailOptions[], + options: BatchRequestOptions = {}, + ): Promise> { return this.send(payload, options); } } diff --git a/src/resources/templates.ts b/src/resources/templates.ts index e11c2c4..7ea546f 100644 --- a/src/resources/templates.ts +++ b/src/resources/templates.ts @@ -41,7 +41,10 @@ export class Templates { } get(idOrAlias: string): Promise> { - return this.http.request({ method: "GET", path: `/templates/${encodeURIComponent(idOrAlias)}` }); + return this.http.request({ + method: "GET", + path: `/templates/${encodeURIComponent(idOrAlias)}`, + }); } /** PATCH /templates/:idOrAlias — `alias`/`subject`/`text: null` clear the field. */ diff --git a/src/resources/topics.ts b/src/resources/topics.ts index d056dda..247febe 100644 --- a/src/resources/topics.ts +++ b/src/resources/topics.ts @@ -39,7 +39,11 @@ export class Topics { return this.http.request({ method: "PATCH", path: `/topics/${encodeURIComponent(id)}`, - body: { name: payload.name, description: payload.description, visibility: payload.visibility }, + body: { + name: payload.name, + description: payload.description, + visibility: payload.visibility, + }, }); } diff --git a/test/client.test.ts b/test/client.test.ts index 58885e9..f66853f 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -67,7 +67,8 @@ describe("construction", () => { expect(() => new MillionSend("ms_test")).toThrow(/allowInsecureHttp/); delete process.env.MILLIONSEND_BASE_URL; expect( - () => new MillionSend("ms_test", { baseUrl: "http://mail.example.com", allowInsecureHttp: true }), + () => + new MillionSend("ms_test", { baseUrl: "http://mail.example.com", allowInsecureHttp: true }), ).not.toThrow(); expect(() => new MillionSend("ms_test", { baseUrl: "http://localhost:3001" })).not.toThrow(); expect(() => new MillionSend("ms_test", { baseUrl: "http://127.0.0.1:3001" })).not.toThrow(); @@ -164,7 +165,11 @@ describe("request wiring", () => { it("passes the all_recipients_suppressed 422 through by name", async () => { const { ms } = makeClient({ status: 422, - body: { statusCode: 422, name: "all_recipients_suppressed", message: "All recipients are suppressed" }, + body: { + statusCode: 422, + name: "all_recipients_suppressed", + message: "All recipients are suppressed", + }, }); const res = await ms.emails.send({ from: "a@x.dev", to: "b@x.dev", subject: "s", text: "t" }); expect(res.data).toBeNull(); diff --git a/test/resources.test.ts b/test/resources.test.ts index edcc3f7..a23be43 100644 --- a/test/resources.test.ts +++ b/test/resources.test.ts @@ -33,8 +33,13 @@ describe("insights", () => { html_size_bytes: 12345, computed_at: "2026-08-31T00:00:00.000Z", checks: [ - { id: "list_unsubscribe", severity: "major", status: "fail", penalty: 1.25, - detail: { header: null, reason: "missing" } }, + { + id: "list_unsubscribe", + severity: "major", + status: "fail", + penalty: 1.25, + detail: { header: null, reason: "missing" }, + }, { id: "plain_text_part", severity: "minor", status: "pass", penalty: 0 }, ], }; @@ -131,7 +136,7 @@ describe("batch", () => { ); expect(pathOf(calls[0])).toBe("/emails/batch"); expect(Array.isArray(calls[0]?.body)).toBe(true); - expect((calls[0]?.body as unknown[]).length).toBe(2); + expect(calls[0]?.body).toHaveLength(2); expect(calls[0]?.headers["Idempotency-Key"]).toBe("batch-1"); expect(res.data?.data.length).toBe(2); }); @@ -163,7 +168,10 @@ describe("contacts", () => { it("remove and list", async () => { const { ms, calls } = makeClient(); await ms.contacts.remove({ email: "c@x.dev" }); - expect(calls[0]).toMatchObject({ method: "DELETE", url: "https://api.test/contacts/c%40x.dev" }); + expect(calls[0]).toMatchObject({ + method: "DELETE", + url: "https://api.test/contacts/c%40x.dev", + }); await ms.contacts.list({ after: "cur" }); expect(pathOf(calls[1])).toBe("/contacts?after=cur"); }); @@ -171,7 +179,10 @@ describe("contacts", () => { it("remove sends ?erase=true only when asked", async () => { const { ms, calls } = makeClient(); await ms.contacts.remove("c1", { erase: true }); - expect(calls[0]).toMatchObject({ method: "DELETE", url: "https://api.test/contacts/c1?erase=true" }); + expect(calls[0]).toMatchObject({ + method: "DELETE", + url: "https://api.test/contacts/c1?erase=true", + }); await ms.contacts.remove("c1", { erase: false }); expect(pathOf(calls[1])).toBe("/contacts/c1"); }); @@ -228,7 +239,9 @@ describe("contacts", () => { it("list passes include as a comma-separated facet list", async () => { const { ms, calls } = makeClient(); await ms.contacts.list({ limit: 100, include: ["properties", "topics"] }); - expect(decodeURIComponent(pathOf(calls[0]))).toBe("/contacts?limit=100&include=properties,topics"); + expect(decodeURIComponent(pathOf(calls[0]))).toBe( + "/contacts?limit=100&include=properties,topics", + ); await ms.contacts.list({ segmentId: "s1", include: ["topics"] }); expect(decodeURIComponent(pathOf(calls[1]))).toBe("/segments/s1/contacts?include=topics"); }); @@ -259,7 +272,10 @@ describe("contacts", () => { body: { data: [{ object: "contact", contact: "c1", deleted: true }] }, }); const res = await ms.contacts.batch.remove({ ids: ["c1", "c2"] }); - expect(calls[0]).toMatchObject({ method: "POST", url: "https://api.test/contacts/batch/remove" }); + expect(calls[0]).toMatchObject({ + method: "POST", + url: "https://api.test/contacts/batch/remove", + }); expect(calls[0]?.body).toEqual({ ids: ["c1", "c2"] }); expect(res.data?.data[0]).toEqual({ object: "contact", contact: "c1", deleted: true }); await ms.contacts.batch.remove({ emails: ["a@x.dev"] }); @@ -272,7 +288,12 @@ describe("contacts", () => { describe("broadcasts", () => { it("covers the full lifecycle", async () => { const { ms, calls } = makeClient(); - await ms.broadcasts.create({ segmentId: "s1", from: "a@x.dev", subject: "News", html: "

hi

" }); + await ms.broadcasts.create({ + segmentId: "s1", + from: "a@x.dev", + subject: "News", + html: "

hi

", + }); expect(pathOf(calls[0])).toBe("/broadcasts"); expect(calls[0]?.body).toEqual({ segment_id: "s1", @@ -408,7 +429,10 @@ describe("emails: every field reaches the wire", () => { it("list, update and remove hit the right paths", async () => { const { ms, calls } = makeClient({ body: { object: "list", data: [], has_more: false } }); await ms.emails.list({ limit: 10, after: "e1" }); - expect(calls[0]).toMatchObject({ method: "GET", url: "https://api.test/emails?limit=10&after=e1" }); + expect(calls[0]).toMatchObject({ + method: "GET", + url: "https://api.test/emails?limit=10&after=e1", + }); await ms.emails.update({ id: "e1", scheduledAt: "2999-01-01T00:00:00Z" }); expect(calls[1]).toMatchObject({ method: "PATCH", url: "https://api.test/emails/e1" }); expect(calls[1]?.body).toEqual({ scheduled_at: "2999-01-01T00:00:00Z" }); @@ -474,7 +498,10 @@ describe("contacts: segments, topics and batch", () => { }; const { ms, calls } = makeClient({ body: response }); const res = await ms.contacts.batch.create( - [{ email: "a@x.dev", firstName: "A" }, { email: "nope", topics: [{ id: "t1", subscription: "opt_out" }] }], + [ + { email: "a@x.dev", firstName: "A" }, + { email: "nope", topics: [{ id: "t1", subscription: "opt_out" }] }, + ], { onConflict: "upsert", batchValidation: "permissive" }, ); expect(calls[0]).toMatchObject({ @@ -500,7 +527,10 @@ describe("contacts: segments, topics and batch", () => { it("segments.add/remove address the contact by email, id or contactId", async () => { const { ms, calls } = makeClient({ body: { id: "c1" } }); await ms.contacts.segments.add({ id: "c1", segmentId: "s1" }); - expect(calls[0]).toMatchObject({ method: "POST", url: "https://api.test/contacts/c1/segments/s1" }); + expect(calls[0]).toMatchObject({ + method: "POST", + url: "https://api.test/contacts/c1/segments/s1", + }); expect(calls[0]?.body).toBeUndefined(); await ms.contacts.segments.remove({ email: "c@x.dev", segmentId: "s1" }); expect(calls[1]).toMatchObject({ @@ -514,7 +544,10 @@ describe("contacts: segments, topics and batch", () => { it("list with segmentId reads /segments/:id/contacts", async () => { const { ms, calls } = makeClient({ body: { object: "list", data: [], has_more: false } }); await ms.contacts.list({ segmentId: "s1", limit: 5 }); - expect(calls[0]).toMatchObject({ method: "GET", url: "https://api.test/segments/s1/contacts?limit=5" }); + expect(calls[0]).toMatchObject({ + method: "GET", + url: "https://api.test/segments/s1/contacts?limit=5", + }); }); it("get returns the typed property objects the API sends", async () => { @@ -577,7 +610,11 @@ describe("broadcasts: full body and null clears", () => { describe("topics: visibility and update", () => { it("create passes visibility; update patches /topics/:id", async () => { const { ms, calls } = makeClient(); - await ms.topics.create({ name: "Product", defaultSubscription: "opt_in", visibility: "public" }); + await ms.topics.create({ + name: "Product", + defaultSubscription: "opt_in", + visibility: "public", + }); expect(calls[0]?.body).toEqual({ name: "Product", default_subscription: "opt_in", @@ -624,10 +661,16 @@ describe("suppressions", () => { it("batch.add and batch.remove post to the batch paths", async () => { const { ms, calls } = makeClient({ body: { data: [] } }); await ms.suppressions.batch.add({ emails: ["a@x.dev", "b@x.dev"], origin: "unsubscribe" }); - expect(calls[0]).toMatchObject({ method: "POST", url: "https://api.test/suppressions/batch/add" }); + expect(calls[0]).toMatchObject({ + method: "POST", + url: "https://api.test/suppressions/batch/add", + }); expect(calls[0]?.body).toEqual({ emails: ["a@x.dev", "b@x.dev"], origin: "unsubscribe" }); await ms.suppressions.batch.remove({ ids: ["sup1"] }); - expect(calls[1]).toMatchObject({ method: "POST", url: "https://api.test/suppressions/batch/remove" }); + expect(calls[1]).toMatchObject({ + method: "POST", + url: "https://api.test/suppressions/batch/remove", + }); expect(calls[1]?.body).toEqual({ ids: ["sup1"] }); await ms.suppressions.batch.remove({ emails: ["a@x.dev"] }); expect(calls[2]?.body).toEqual({ emails: ["a@x.dev"] }); @@ -736,7 +779,11 @@ describe("webhooks", () => { describe("apiKeys", () => { it("create returns the token once; list and remove", async () => { const { ms, calls } = makeClient({ body: { id: "k1", token: "ms_secret" } }); - const res = await ms.apiKeys.create({ name: "ci", permission: "sending_access", domainId: "d1" }); + const res = await ms.apiKeys.create({ + name: "ci", + permission: "sending_access", + domainId: "d1", + }); expect(calls[0]).toMatchObject({ method: "POST", url: "https://api.test/api-keys" }); expect(calls[0]?.body).toEqual({ name: "ci", permission: "sending_access", domain_id: "d1" }); expect(res.data?.token).toBe("ms_secret"); @@ -767,14 +814,25 @@ describe("templates", () => { text: "Hi", alias: "welcome", }); - await ms.templates.update("welcome", { alias: null, subject: null, text: null, html: "

Yo

" }); + await ms.templates.update("welcome", { + alias: null, + subject: null, + text: null, + html: "

Yo

", + }); expect(calls[1]).toMatchObject({ method: "PATCH", url: "https://api.test/templates/welcome" }); expect(calls[1]?.body).toEqual({ html: "

Yo

", subject: null, text: null, alias: null }); }); it("passes from/replyTo/variables through unchanged (the API decides)", async () => { const { ms, calls } = makeClient(); - await ms.templates.create({ name: "n", html: "

", from: "a@x.dev", replyTo: ["r@x.dev"], variables: [] }); + await ms.templates.create({ + name: "n", + html: "

", + from: "a@x.dev", + replyTo: ["r@x.dev"], + variables: [], + }); expect(calls[0]?.body).toEqual({ name: "n", html: "

", @@ -791,9 +849,15 @@ describe("templates", () => { await ms.templates.get("welcome"); expect(calls[1]).toMatchObject({ method: "GET", url: "https://api.test/templates/welcome" }); await ms.templates.publish("tp1"); - expect(calls[2]).toMatchObject({ method: "POST", url: "https://api.test/templates/tp1/publish" }); + expect(calls[2]).toMatchObject({ + method: "POST", + url: "https://api.test/templates/tp1/publish", + }); await ms.templates.duplicate("tp1"); - expect(calls[3]).toMatchObject({ method: "POST", url: "https://api.test/templates/tp1/duplicate" }); + expect(calls[3]).toMatchObject({ + method: "POST", + url: "https://api.test/templates/tp1/duplicate", + }); await ms.templates.remove("tp1"); expect(calls[4]).toMatchObject({ method: "DELETE", url: "https://api.test/templates/tp1" }); }); @@ -806,14 +870,26 @@ describe("contactProperties", () => { expect(calls[0]).toMatchObject({ method: "POST", url: "https://api.test/contact-properties" }); expect(calls[0]?.body).toEqual({ key: "plan", type: "string", fallback_value: "free" }); await ms.contactProperties.list({ limit: 3 }); - expect(calls[1]).toMatchObject({ method: "GET", url: "https://api.test/contact-properties?limit=3" }); + expect(calls[1]).toMatchObject({ + method: "GET", + url: "https://api.test/contact-properties?limit=3", + }); await ms.contactProperties.get("p1"); - expect(calls[2]).toMatchObject({ method: "GET", url: "https://api.test/contact-properties/p1" }); + expect(calls[2]).toMatchObject({ + method: "GET", + url: "https://api.test/contact-properties/p1", + }); await ms.contactProperties.update({ id: "p1", fallbackValue: null }); - expect(calls[3]).toMatchObject({ method: "PATCH", url: "https://api.test/contact-properties/p1" }); + expect(calls[3]).toMatchObject({ + method: "PATCH", + url: "https://api.test/contact-properties/p1", + }); expect(calls[3]?.body).toEqual({ fallback_value: null }); await ms.contactProperties.remove("p1"); - expect(calls[4]).toMatchObject({ method: "DELETE", url: "https://api.test/contact-properties/p1" }); + expect(calls[4]).toMatchObject({ + method: "DELETE", + url: "https://api.test/contact-properties/p1", + }); }); });