diff --git a/jest.config.js b/jest.config.js index 6f0dfa6..bedc6dc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ */ const config = { -// preset: 'ts-jest', + // preset: 'ts-jest', testEnvironment: 'node', verbose: true, transform: { @@ -18,11 +18,12 @@ const config = { // modulePathIgnorePatterns: ['/node_modules/'], clearMocks: true, collectCoverage: false, -// coverageDirectory: 'coverage', -// coverageReporters: ['html'], + // coverageDirectory: 'coverage', + // coverageReporters: ['html'], moduleNameMapper: { '^(.+)\\.js$': '$1', }, + setupFilesAfterEnv: ['./jest.setup.js'], }; export default config; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..ced9c6d --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,2 @@ +// jest.setup.js +require('jest-fetch-mock').enableMocks(); diff --git a/package-lock.json b/package-lock.json index 0a9a46e..2bfb51c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "babel-jest": "^29.7.0", "eslint": "^8.48.0", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.0.3", "ts-jest": "^29.2.4", "typescript": "^5.2.2" @@ -3398,6 +3399,35 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4697,6 +4727,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -5658,6 +5698,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6173,6 +6219,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/ts-api-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", @@ -6398,6 +6450,22 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8977,6 +9045,26 @@ "prompts": "^2.0.1" } }, + "cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "requires": { + "node-fetch": "^2.6.12" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9915,6 +10003,16 @@ "jest-util": "^29.7.0" } }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -10639,6 +10737,12 @@ } } }, + "promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11004,6 +11108,12 @@ "is-number": "^7.0.0" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "ts-api-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", @@ -11135,6 +11245,22 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 5f19e29..c9100b1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test-e2e-clean": "jest --clearCache && jest --showConfig && jest --verbose --config ./jest.config.js", "test-e2e": "jest --verbose --config ./jest.config.js", "test-e2e-watch": "jest --verbose --config ./jest.config.js --watch", + "test-watch": "jest --verbose --config ./jest.config.js --watch", "prepublishOnly": "tsc", "postversion": "git push origin" }, @@ -30,6 +31,7 @@ "babel-jest": "^29.7.0", "eslint": "^8.48.0", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.0.3", "ts-jest": "^29.2.4", "typescript": "^5.2.2" diff --git a/src/request-sender.ts b/src/request-sender.ts index d53e155..5fcd431 100644 --- a/src/request-sender.ts +++ b/src/request-sender.ts @@ -29,6 +29,13 @@ export class RequestSender { parameters: input.parameters ?? {}, }); + // console.log('url', url, { + // protocol: this.clientState.protocol, + // domain: this.clientState.domain, + // apiVersion: this.clientState.apiVersion, + // path: input.path, + // parameters: input.parameters ?? {}, + // }); let parsedBody; if (input.body !== undefined && input.headers?.['Content-Type'] === 'application/json') parsedBody = JSON.stringify(input.body); @@ -76,10 +83,10 @@ export class RequestSender { let formattedParameters: string; if (Object.keys(parameters).length === 0) formattedParameters = ''; else { - /** - * @todo Check if we need to something about + signs ? - * cf. https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs - */ + /** + * @todo Check if we need to something about + signs ? + * cf. https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams#preserving_plus_signs + */ formattedParameters = '?' + new URLSearchParams(parameters).toString(); } diff --git a/src/resources/account.resource.ts b/src/resources/account.resource.ts index 8898055..fce1c16 100644 --- a/src/resources/account.resource.ts +++ b/src/resources/account.resource.ts @@ -37,6 +37,7 @@ export class AccountResource { const { limit, cursor } = input; const parameters: Record = { ...options?.extra_params }; + // console.log('getAll', options, parameters); if (limit) parameters.limit = String(limit); if (cursor) parameters.cursor = cursor; diff --git a/src/resources/email.resource.ts b/src/resources/email.resource.ts index 4efc31e..9c0d9f5 100644 --- a/src/resources/email.resource.ts +++ b/src/resources/email.resource.ts @@ -283,10 +283,6 @@ export class EmailResource { input; const formDataBody = new FormData(); - if (options?.extra_params) { - Object.entries(options.extra_params).forEach(([k, v]) => formDataBody.append(k, v)); - } - formDataBody.append('body', body); formDataBody.append('account_id', account_id); if (draft_id) formDataBody.append('draft_id', draft_id); @@ -321,6 +317,13 @@ export class EmailResource { formDataBody.append('reply_to', reply_to); } + if (options?.extra_params) { + Object.entries(options.extra_params).forEach(([k, v]) => { + if (!formDataBody.has(k)) { + formDataBody.append(k, v); + } + }); + } return await this.client.request.send({ path: ['emails'], method: 'POST', diff --git a/src/resources/extra_params.spec.ts b/src/resources/extra_params.spec.ts new file mode 100644 index 0000000..f2ce293 --- /dev/null +++ b/src/resources/extra_params.spec.ts @@ -0,0 +1,125 @@ +// import { AccountListResponseValidator } from "../accounts/accounts-list.schema.js"; +import { UnipileClient } from '../client.js'; +import { enableFetchMocks } from 'jest-fetch-mock'; +import fetchMock from 'jest-fetch-mock'; +/** */ +//------------------------------------------------------------------------------ +describe('extra_params', () => { + let client: UnipileClient; + + const BASE_URL = 'http://test'; + const API_URL = BASE_URL + '/api/v1/'; + + beforeAll(async () => { + enableFetchMocks(); + }); + + beforeEach(async () => { + client = new UnipileClient(BASE_URL, 'ACCESS_TOKEN', { + logRequestPayload: false, + logRequestResult: false, + validateRequestPayload: false, + // validateRequestPayloadLevel: 'error', + }); + + fetchMock.resetMocks(); + fetchMock.mockResponse(async (req) => ({ + body: JSON.stringify({ url: req.url }), + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + })); + }); + + //---------------------------------------------------------------------------- + describe('GET', () => { + //-------------------------------------------------------------------------- + it('should inject extra_params as query string ' + 'on GET ' + 'when no input params', async () => { + const r = await client.account.getOne('some_account_id', { extra_params: { xtra: 'test' } }); + // console.log(r); + expect((r as any).url).toBe(API_URL + 'accounts/some_account_id?xtra=test'); + }); + //-------------------------------------------------------------------------- + it( + 'should inject extra_params as query string ' + + 'and not overwrite input params ' + + 'on GET ' + + 'when mixed with input params', + async () => { + const r = await client.account.getAll({ limit: 30 }, { extra_params: { limit: 'garbage', legit: 'test' } }); + // console.log(r); + expect((r as any).url).toBe(API_URL + 'accounts?limit=30&legit=test'); + }, + ); + }); + //---------------------------------------------------------------------------- + describe('POST | PUT', () => { + //-------------------------------------------------------------------------- + it.skip('should inject extra_params in body ' + 'on POST ' + 'when no input params', async () => {}); + //-------------------------------------------------------------------------- + it( + 'should inject extra_params in body ' + + 'and not overwrite input params ' + + 'on POST ' + + 'when body Content-Type is application/json ' + + 'and mixed with input params', + async () => { + const r = await client.account.connect( + { provider: 'WHATSAPP' }, + { extra_params: { provider: 'garbage', legit: 'test' } }, + ); + console.log(r); + expect((r as any).url).toBe(API_URL + 'accounts'); + expect(fetchMock.mock.calls[0]?.[1]?.body).toBe(JSON.stringify({ provider: 'WHATSAPP', legit: 'test' })); + }, + ); + //-------------------------------------------------------------------------- + it( + 'should inject extra_params in body ' + + 'and not overwrite input params ' + + 'on POST ' + + 'when body Content-Type is multipart/form-data ' + + 'and mixed with input params', + async () => { + const r = await client.email.send( + { account_id: 'some_account_id', to: [{ identifier: 'att@mail.com' }], body: 'content' }, + { extra_params: { account_id: 'garbage', legit: 'test' } }, + ); + + const expectedFormData = [ + // ['account_id', 'garbage'], + ['account_id', 'some_account_id'], + ['legit', 'test'], + ['body', 'content'], + ['to', '[{"identifier":"att@mail.com"}]'], + ]; + expect((r as any).url).toBe(API_URL + 'emails'); + const resultFormData = [...fetchMock.mock.calls[0]?.[1]?.body.entries()]; + // console.log(r, resultFormData); + expect(resultFormData).toEqual(expect.arrayContaining(expectedFormData)); + expect(resultFormData.length).toBe(expectedFormData.length); + }, + ); + }); + //---------------------------------------------------------------------------- + describe('DELETE', () => { + //-------------------------------------------------------------------------- + it('should inject extra_params as query string ' + 'on DELETE ' + 'when no input params', async () => { + const r = await client.account.delete('some_account_id', { extra_params: { xtra: 'test' } }); + // console.log(r); + expect((r as any).url).toBe(API_URL + 'accounts/some_account_id?xtra=test'); + }); + //-------------------------------------------------------------------------- + it( + 'should inject extra_params as query string ' + + 'and not overwrite input params ' + + 'on DELETE ' + + 'when mixed with input params', + async () => { + const r = await client.email.delete.byProviderId('email-id', 'some_account_id', { + extra_params: { account_id: 'garbage', legit: 'test' }, + }); + // console.log(r); + expect((r as any).url).toBe(API_URL + 'emails/email-id?account_id=some_account_id&legit=test'); + }, + ); + }); +}); diff --git a/src/resources/messaging.resource.ts b/src/resources/messaging.resource.ts index bd8c603..67caf18 100644 --- a/src/resources/messaging.resource.ts +++ b/src/resources/messaging.resource.ts @@ -89,10 +89,6 @@ export class MessagingResource { const { chat_id, text, thread_id, attachments } = input; const body = new FormData(); - if (options?.extra_params) { - Object.entries(options.extra_params).forEach(([k, v]) => body.append(k, v)); - } - body.append('text', text); if (thread_id) body.append('thread_id', thread_id); @@ -102,6 +98,14 @@ export class MessagingResource { } } + if (options?.extra_params) { + Object.entries(options.extra_params).forEach(([k, v]) => { + if (!body.has(k)) { + body.append(k, v); + } + }); + } + return await this.client.request.send({ path: ['chats', chat_id, 'messages'], method: 'POST', @@ -118,10 +122,6 @@ export class MessagingResource { const { account_id, text, subject, options: input_options, attendees_ids, attachments } = input; const body = new FormData(); - if (options?.extra_params) { - Object.entries(options.extra_params).forEach(([k, v]) => body.append(k, v)); - } - body.append('account_id', account_id); body.append('text', text); for (const id of attendees_ids) body.append('attendees_ids', id); @@ -162,6 +162,14 @@ export class MessagingResource { } } + if (options?.extra_params) { + Object.entries(options.extra_params).forEach(([k, v]) => { + if (!body.has(k)) { + body.append(k, v); + } + }); + } + return await this.client.request.send({ path: ['chats'], method: 'POST', diff --git a/src/resources/users.resource.ts b/src/resources/users.resource.ts index a1129c7..3e6dcb9 100644 --- a/src/resources/users.resource.ts +++ b/src/resources/users.resource.ts @@ -152,10 +152,6 @@ export class UsersResource { const { account_id, text, attachments } = input; const body = new FormData(); - if (options?.extra_params) { - Object.entries(options.extra_params).forEach(([k, v]) => body.append(k, v)); - } - body.append('text', text); if (account_id) body.append('account_id', account_id); @@ -165,6 +161,14 @@ export class UsersResource { } } + if (options?.extra_params) { + Object.entries(options.extra_params).forEach(([k, v]) => { + if (!body.has(k)) { + body.append(k, v); + } + }); + } + return await this.client.request.send({ path: ['posts'], method: 'POST', diff --git a/src/tests-e2e/email.spec.ts b/src/tests-e2e/email.spec.ts index 63a6968..45222bd 100644 --- a/src/tests-e2e/email.spec.ts +++ b/src/tests-e2e/email.spec.ts @@ -22,7 +22,7 @@ describe("EmailResource", () => { describe("getAll", () => { //-------------------------------------------------------------------------- it( - "should return return a validated EmailList " + + "should return a validated EmailList " + "on getAll " + "when account_id", async () => { diff --git a/src/tests-e2e/user.spec.ts b/src/tests-e2e/user.spec.ts index d21162a..2eace82 100644 --- a/src/tests-e2e/user.spec.ts +++ b/src/tests-e2e/user.spec.ts @@ -1,4 +1,5 @@ import { UnipileClient } from "../client.js"; +import { RequestOptions } from "../types/request.js"; import { config } from "./instance.config.js"; /** */ @@ -7,6 +8,11 @@ describe("UserResource", () => { let client: UnipileClient; // beforeAll(async () => {}); + const extra_params: NonNullable = { + a: "3", + b: "test$ !H/n,9", + }; + beforeEach(async () => { client = new UnipileClient(config.BASE_URL, config.ACCESS_TOKEN, { logRequestPayload: config.logRequestPayload, @@ -19,13 +25,16 @@ describe("UserResource", () => { //---------------------------------------------------------------------------- describe("getProfile", () => { //-------------------------------------------------------------------------- - it( + it.only( "should return UserProfile for any account provider" + "on getProfile " + "when identifier", async () => { try { - const accounts = await client.account.getAll({ limit: 1 }); + const accounts = await client.account.getAll( + { limit: 1 }, + { extra_params }, + ); // accounts.items = [accounts.items[0]]; // console.log( // ...AccountListResponseValidator.Errors(accounts),