diff --git a/packages/twenty-server/@types/jest.d.ts b/packages/twenty-server/@types/jest.d.ts index 2b06c7722..d74d9502d 100644 --- a/packages/twenty-server/@types/jest.d.ts +++ b/packages/twenty-server/@types/jest.d.ts @@ -6,6 +6,7 @@ declare module '@jest/types' { APP_PORT: number; ADMIN_ACCESS_TOKEN: string; EXPIRED_ACCESS_TOKEN: string; + INVALID_ACCESS_TOKEN: string; MEMBER_ACCESS_TOKEN: string; GUEST_ACCESS_TOKEN: string; } @@ -16,6 +17,7 @@ declare global { const APP_PORT: number; const ADMIN_ACCESS_TOKEN: string; const EXPIRED_ACCESS_TOKEN: string; + const INVALID_ACCESS_TOKEN: string; const MEMBER_ACCESS_TOKEN: string; const GUEST_ACCESS_TOKEN: string; } diff --git a/packages/twenty-server/jest-integration.config.ts b/packages/twenty-server/jest-integration.config.ts index c5b8a5bbb..a8c7f9b39 100644 --- a/packages/twenty-server/jest-integration.config.ts +++ b/packages/twenty-server/jest-integration.config.ts @@ -70,6 +70,8 @@ const jestConfig: JestConfigWithTsJest = { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw', EXPIRED_ACCESS_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzM4MzIzODc5LCJleHAiOjE3MzgzMjU2Nzl9.m73hHVpnw5uGNGrSuKxn6XtKEUK3Wqkp4HsQdYfZiHo', + INVALID_ACCESS_TOKEN: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzM4MzIzODc5LCJleHAiOjE3MzgzMjU2Nzl9.m73hHVpnw5uGNGrSuKxn6XtKEUK3Wqkp4HsQdYfZiHp', MEMBER_ACCESS_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0zOTU3LTQ5MDgtOWMzNi0yOTI5YTIzZjgzNTciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNzdkNS00Y2I2LWI2MGEtZjRhODM1YTg1ZDYxIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMzk1Ny00OTA4LTljMzYtMjkyOWEyM2Y4MzUzIiwiaWF0IjoxNzM5NDU5NTcwLCJleHAiOjMzMjk3MDU5NTcwfQ.Er7EEU4IP4YlGN79jCLR_6sUBqBfKx2M3G_qGiDpPRo', GUEST_ACCESS_TOKEN: diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index 6ed4d0134..371797d8e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { Request } from 'express'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -56,6 +56,18 @@ export class RestApiCoreServiceV2 { objectMetadataMapItem: objectMetadata.objectMetadataMapItem, }); + const recordExists = + isDefined(overriddenBody.id) && + (await repository.exists({ + where: { + id: overriddenBody.id, + }, + })); + + if (recordExists) { + throw new BadRequestException('Record already exists'); + } + const createdRecord = await repository.save(overriddenBody); this.apiEventEmitterService.emitCreateEvents( diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index cd65d2a1f..17df32f37 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -128,7 +128,7 @@ export class AccessTokenService { if (!token) { throw new AuthException( - 'missing authentication token', + 'Missing authentication token', AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } diff --git a/packages/twenty-server/test/integration/constants/mock-person-ids.constants.ts b/packages/twenty-server/test/integration/constants/mock-person-ids.constants.ts index c81e4c826..b7816bd60 100644 --- a/packages/twenty-server/test/integration/constants/mock-person-ids.constants.ts +++ b/packages/twenty-server/test/integration/constants/mock-person-ids.constants.ts @@ -1,4 +1,4 @@ export const PERSON_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; export const PERSON_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; export const PERSON_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; -export const FAKE_PERSON_ID = '777a8457-eb2d-40ac-a707-551b615b6990'; +export const NOT_EXISTING_PERSON_ID = '777a8457-eb2d-40ac-a707-551b615b6990'; diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-authentication.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-authentication.integration-spec.ts new file mode 100644 index 000000000..098d16e9a --- /dev/null +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-authentication.integration-spec.ts @@ -0,0 +1,55 @@ +import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; + +describe('Core REST API Authentication', () => { + it('should return an UnauthorizedException when no token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + bearer: '', + }) + .expect(400) + .expect((res) => { + expect(res.body.error).toBe('FORBIDDEN_EXCEPTION'); + expect(res.body.messages[0]).toBe('Missing authentication token'); + }); + }); + + it('should return an Unauthenticated when an invalid token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + bearer: INVALID_ACCESS_TOKEN, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + expect(res.body.messages[0]).toBe('Token invalid.'); + }); + }); + + it('should return an Unauthenticated when no token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + bearer: 'invalid-token', + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + expect(res.body.messages[0]).toBe('No payload'); + }); + }); + + it('should return an Unauthenticated when an expired token is provided', async () => { + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + bearer: EXPIRED_ACCESS_TOKEN, + }) + .expect(401) + .expect((res) => { + expect(res.body.error).toBe('UNAUTHENTICATED'); + expect(res.body.messages[0]).toBe('Token has expired.'); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts index d1b3bc5fd..df51ca04f 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-create-one.integration-spec.ts @@ -1,41 +1,41 @@ -import { - FAKE_PERSON_ID, - PERSON_2_ID, -} from 'test/integration/constants/mock-person-ids.constants'; +import { PERSON_1_ID } from 'test/integration/constants/mock-person-ids.constants'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { generateRecordName } from 'test/integration/utils/generate-record-name'; -describe.skip('Core REST API Create One endpoint', () => { - afterAll(async () => { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_2_ID}`, - }).expect(200); - }); +describe('Core REST API Create One endpoint', () => { + beforeAll( + async () => + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + }), + ); - it('2.a. should create a new person', async () => { - const personCity = generateRecordName(PERSON_2_ID); + it('should create a new person', async () => { + const personCity = generateRecordName(PERSON_1_ID); const requestBody = { - id: PERSON_2_ID, + id: PERSON_1_ID, city: personCity, }; - const response = await makeRestAPIRequest({ + await makeRestAPIRequest({ method: 'post', path: `/people`, body: requestBody, - }); + }) + .expect(201) + .expect((res) => { + const createdPerson = res.body.data.createPerson; - const createdPerson = response.body.data.createPerson; - - expect(createdPerson.id).toBe(PERSON_2_ID); - expect(createdPerson.city).toBe(personCity); + expect(createdPerson.id).toBe(PERSON_1_ID); + expect(createdPerson.city).toBe(personCity); + }); }); - it('2.b. should return a BadRequestException when trying to create a person with an existing ID', async () => { - const personCity = generateRecordName(PERSON_2_ID); + it('should return a BadRequestException when trying to create a person with an existing ID', async () => { + const personCity = generateRecordName(PERSON_1_ID); const requestBody = { - id: PERSON_2_ID, + id: PERSON_1_ID, city: personCity, }; @@ -46,50 +46,8 @@ describe.skip('Core REST API Create One endpoint', () => { }) .expect(400) .expect((res) => { - expect(res.body.messages[0]).toContain( - `duplicate key value violates unique constraint`, - ); - expect(res.body.error).toBe('QueryFailedError'); - }); - }); - - it('2.c. should return an UnauthorizedException when no token is provided', async () => { - await makeRestAPIRequest({ - method: 'post', - path: `/people`, - headers: { authorization: '' }, - body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('2.d. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest({ - method: 'post', - path: `/people`, - body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, - headers: { authorization: 'Bearer invalid-token' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('2.e. should return an UnauthorizedException when an expired token is provided', async () => { - await makeRestAPIRequest({ - method: 'post', - path: `/people`, - body: { id: FAKE_PERSON_ID, city: 'FakeCity' }, - headers: { authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}` }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - expect(res.body.messages[0]).toBe('Token has expired.'); + expect(res.body.messages[0]).toContain(`Record already exists`); + expect(res.body.error).toBe('BadRequestException'); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts index ff6cf123e..d79c472a9 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts @@ -1,115 +1,41 @@ import { - FAKE_PERSON_ID, + NOT_EXISTING_PERSON_ID, PERSON_1_ID, } from 'test/integration/constants/mock-person-ids.constants'; -import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; -import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; -describe.skip('Core REST API Delete One endpoint', () => { - let people: any; - - beforeAll(async () => { - const personCity1 = generateRecordName(PERSON_1_ID); - - const response = await makeRestAPIRequest({ - method: 'post', - path: '/people', - body: { - id: PERSON_1_ID, - city: personCity1, - }, - }); - - people = response.body.data.createPeople; - expect(people.length).toBe(1); - expect(people[0].id).toBe(PERSON_1_ID); - }); - - afterAll(async () => { - // TODO: move this creation to REST API when the GET method is migrated - const graphqlOperation = findOneOperationFactory({ - objectMetadataSingularName: 'person', - gqlFields: PERSON_GQL_FIELDS, - filter: { - id: { - eq: PERSON_1_ID, +describe('Core REST API Delete One endpoint', () => { + beforeAll( + async () => + await makeRestAPIRequest({ + method: 'post', + path: `/people`, + body: { + id: PERSON_1_ID, }, - }, - }); + }), + ); - await makeGraphqlAPIRequest(graphqlOperation) - .expect(400) - .expect((res) => { - expect(res.body.error.message).toContain(`Record not found`); - }); - }); - - it('1a. should delete one person', async () => { - const response = await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_1_ID}`, - }); - - expect(response.body.data.deletePerson.id).toBe(PERSON_1_ID); - }); - - it('1.b. should return a BadRequestException when trying to delete a non-existing person', async () => { + it('should delete one person', async () => { await makeRestAPIRequest({ method: 'delete', - path: `/people/${FAKE_PERSON_ID}`, + path: `/people/${PERSON_1_ID}`, + }) + .expect(200) + .expect((res) => expect(res.body.data.deletePerson.id).toBe(PERSON_1_ID)); + }); + + it('should return a EntityNotFoundError when trying to delete a non-existing person', async () => { + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${NOT_EXISTING_PERSON_ID}`, }) .expect(400) .expect((res) => { expect(res.body.messages[0]).toContain( `Could not find any entity of type "person"`, ); - expect(res.body.error).toBe('Bad Request'); - }); - }); - - it('1.c. should return an UnauthorizedException when no token is provided', async () => { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_1_ID}`, - headers: { - authorization: '', - }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('1.d. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_1_ID}`, - headers: { - authorization: 'Bearer invalid-token', - }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - }); - }); - - it('1.e. should return an UnauthorizedException when an expired token is provided', async () => { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_1_ID}`, - headers: { - authorization: `Bearer ${EXPIRED_ACCESS_TOKEN}`, - }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); - expect(res.body.messages[0]).toBe('Token has expired.'); // Adjust this based on your API's error response + expect(res.body.error).toBe('EntityNotFoundError'); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts index 92d248e1f..fdff0713c 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-update.integration-spec.ts @@ -1,32 +1,26 @@ -import { INITIAL_PERSON_DATA } from 'test/integration/constants/initial-person-data.constants'; import { - FAKE_PERSON_ID, - PERSON_2_ID, + NOT_EXISTING_PERSON_ID, + PERSON_1_ID, } from 'test/integration/constants/mock-person-ids.constants'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { generateRecordName } from 'test/integration/utils/generate-record-name'; -describe.skip('Core REST API Update One endpoint', () => { - let initialPersonData; - +describe('Core REST API Update One endpoint', () => { beforeAll(async () => { - initialPersonData = INITIAL_PERSON_DATA; - + await makeRestAPIRequest({ + method: 'delete', + path: `/people/${PERSON_1_ID}`, + }); await makeRestAPIRequest({ method: 'post', path: `/people`, - body: initialPersonData, - }).expect(200); + body: { + id: PERSON_1_ID, + }, + }); }); - afterAll(async () => { - await makeRestAPIRequest({ - method: 'delete', - path: `/people/${PERSON_2_ID}`, - }).expect(200); - }); - - it('3.a. should update an existing person (name, emails, and city)', async () => { + it('should update an existing person (name, emails, and city)', async () => { const updatedData = { name: { firstName: 'Updated', @@ -36,55 +30,45 @@ describe.skip('Core REST API Update One endpoint', () => { primaryEmail: 'updated@example.com', additionalEmails: ['extra@example.com'], }, - city: generateRecordName(PERSON_2_ID), + city: generateRecordName(PERSON_1_ID), }; - const response = await makeRestAPIRequest({ - method: 'patch', - path: `/people/${PERSON_2_ID}`, - body: updatedData, - }).expect(200); - - const updatedPerson = response.body.data.updatePerson; - - expect(updatedPerson.id).toBe(PERSON_2_ID); - expect(updatedPerson.name.firstName).toBe(updatedData.name.firstName); - expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName); - expect(updatedPerson.emails.primaryEmail).toBe( - updatedData.emails.primaryEmail, - ); - expect(updatedPerson.emails.additionalEmails).toEqual( - updatedData.emails.additionalEmails, - ); - expect(updatedPerson.city).toBe(updatedData.city); - - expect(updatedPerson.jobTitle).toBe(initialPersonData.jobTitle); - expect(updatedPerson.companyId).toBe(initialPersonData.companyId); - }); - - it('3.b. should return a BadRequestException when trying to update a non-existing person', async () => { await makeRestAPIRequest({ method: 'patch', - path: `/people/${FAKE_PERSON_ID}`, - body: { city: 'NonExistingCity' }, + path: `/people/${PERSON_1_ID}`, + body: updatedData, + }) + .expect(200) + .expect((res) => { + const updatedPerson = res.body.data.updatePerson; + + expect(updatedPerson.id).toBe(PERSON_1_ID); + expect(updatedPerson.name.firstName).toBe(updatedData.name.firstName); + expect(updatedPerson.name.lastName).toBe(updatedData.name.lastName); + expect(updatedPerson.emails.primaryEmail).toBe( + updatedData.emails.primaryEmail, + ); + expect(updatedPerson.emails.additionalEmails).toEqual( + updatedData.emails.additionalEmails, + ); + expect(updatedPerson.city).toBe(updatedData.city); + + expect(updatedPerson.jobTitle).toBe(''); + expect(updatedPerson.companyId).toBe(null); + }); + }); + + it('should return a EntityNotFoundError when trying to update a non-existing person', async () => { + await makeRestAPIRequest({ + method: 'patch', + path: `/people/${NOT_EXISTING_PERSON_ID}`, }) .expect(400) .expect((res) => { - expect(res.body.error).toBe('BadRequestException'); - expect(res.body.messages[0]).toContain('Record ID not found'); - }); - }); - - it('3.c. should return an UnauthorizedException when an invalid token is provided', async () => { - await makeRestAPIRequest({ - method: 'patch', - path: `/people/${PERSON_2_ID}`, - headers: { authorization: 'Bearer invalid-token' }, - body: { city: 'InvalidTokenCity' }, - }) - .expect(401) - .expect((res) => { - expect(res.body.error).toBe('UNAUTHENTICATED'); + expect(res.body.messages[0]).toContain( + `Could not find any entity of type "person"`, + ); + expect(res.body.error).toBe('EntityNotFoundError'); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts b/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts index 1e8c0c9a1..5522adbe7 100644 --- a/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/make-rest-api-request.util.ts @@ -1,5 +1,3 @@ -import { IncomingHttpHeaders } from 'http'; - import request from 'supertest'; export type RestAPIRequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; @@ -7,20 +5,26 @@ export type RestAPIRequestMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; interface RestAPIRequestParams { method: RestAPIRequestMethod; path: string; - headers?: IncomingHttpHeaders; + bearer?: string; body?: any; } export const makeRestAPIRequest = ({ method, path, - headers = {}, - body, + bearer = ADMIN_ACCESS_TOKEN, + body = {}, }: RestAPIRequestParams) => { const client = request(`http://localhost:${APP_PORT}`); - return client[method](`/rest${path}`) - .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) - .set(headers) - .send(body ? JSON.stringify(body) : undefined); + const req = client[method](`/rest${path}`).set( + 'Authorization', + `Bearer ${bearer}`, + ); + + if (['post', 'patch', 'put'].includes(method)) { + req.set('Content-Type', 'application/json').send(JSON.stringify(body)); + } + + return req; };