From db6758f7f7dcd523cb91ba41f5d162e4b2020ba1 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 15 Feb 2024 10:01:13 -0500 Subject: [PATCH] chore: adds rest client for Next handlers --- .env.example | 4 + __mocks__/payload-config.ts | 3 - jest.config.js | 3 - packages/next/src/routes/graphql/handler.ts | 93 ++++----- packages/next/src/routes/rest/RouteError.ts | 66 +++--- packages/next/src/routes/rest/index.ts | 8 +- .../src/utilities/createPayloadRequest.ts | 2 +- packages/ui/tsconfig.json | 5 +- test/_community/int.spec.ts | 70 +++---- test/auth/config.ts | 20 +- test/auth/int.spec.ts | 193 ++++++------------ test/helpers/NextRESTClient.ts | 94 +++++++++ tsconfig.json | 69 ++----- 13 files changed, 300 insertions(+), 330 deletions(-) create mode 100644 .env.example delete mode 100644 __mocks__/payload-config.ts create mode 100644 test/helpers/NextRESTClient.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..940af56b36 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +DATABASE_URI=mongodb://127.0.0.1/payloadtests +PAYLOAD_SECRET=laijflieawfjlweifjewalifjwe +# PAYLOAD_CONFIG_PATH=MUST BE SET PROGRAMMATICALLY +PAYLOAD_DROP_DATABASE=true diff --git a/__mocks__/payload-config.ts b/__mocks__/payload-config.ts deleted file mode 100644 index 9869c394b2..0000000000 --- a/__mocks__/payload-config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default typeof process.env.PAYLOAD_CONFIG_PATH === 'string' - ? require(process.env.PAYLOAD_CONFIG_PATH) - : {} diff --git a/jest.config.js b/jest.config.js index d56dfff7b2..54432e6205 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,9 +15,6 @@ const customJestConfig = { // testEnvironment: 'node', testMatch: ['/packages/payload/src/**/*.spec.ts', '/test/**/*int.spec.ts'], testTimeout: 90000, - // transform: { - // '^.+\\.(t|j)sx?$': ['@swc/jest'], - // }, verbose: true, } diff --git a/packages/next/src/routes/graphql/handler.ts b/packages/next/src/routes/graphql/handler.ts index 23c48c2ac6..1f9b4c1a2b 100644 --- a/packages/next/src/routes/graphql/handler.ts +++ b/packages/next/src/routes/graphql/handler.ts @@ -48,7 +48,7 @@ if (!cached) { cached = global._payload_graphql = { graphql: null, promise: null } } -export const getGraphql = async (config: Promise) => { +export const getGraphql = async (config: Promise | SanitizedConfig) => { if (cached.graphql) { return cached.graphql } @@ -71,53 +71,54 @@ export const getGraphql = async (config: Promise) => { return cached.graphql } -export const POST = (config: Promise) => async (request: Request) => { - const originalRequest = request.clone() - const req = await createPayloadRequest({ - request, - config, - }) - const { schema, validationRules } = await getGraphql(config) +export const POST = + (config: Promise | SanitizedConfig) => async (request: Request) => { + const originalRequest = request.clone() + const req = await createPayloadRequest({ + request, + config, + }) + const { schema, validationRules } = await getGraphql(config) - const { payload } = req + const { payload } = req - const afterErrorHook = - typeof payload.config.hooks.afterError === 'function' ? payload.config.hooks.afterError : null + const afterErrorHook = + typeof payload.config.hooks.afterError === 'function' ? payload.config.hooks.afterError : null - const headers = {} - const apiResponse = await createHandler({ - context: { req, headers }, - onOperation: async (request, args, result) => { - const response = - typeof payload.extensions === 'function' - ? await payload.extensions({ - args, - req: request, - result, - }) - : result - if (response.errors) { - const errors = (await Promise.all( - result.errors.map((error) => { - return handleError(payload, error, payload.config.debug, afterErrorHook) - }), - )) as GraphQLError[] - // errors type should be FormattedGraphQLError[] but onOperation has a return type of ExecutionResult instead of FormattedExecutionResult - return { ...response, errors } - } - return response - }, - schema: schema, - validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)), - })(originalRequest) + const headers = {} + const apiResponse = await createHandler({ + context: { req, headers }, + onOperation: async (request, args, result) => { + const response = + typeof payload.extensions === 'function' + ? await payload.extensions({ + args, + req: request, + result, + }) + : result + if (response.errors) { + const errors = (await Promise.all( + result.errors.map((error) => { + return handleError(payload, error, payload.config.debug, afterErrorHook) + }), + )) as GraphQLError[] + // errors type should be FormattedGraphQLError[] but onOperation has a return type of ExecutionResult instead of FormattedExecutionResult + return { ...response, errors } + } + return response + }, + schema: schema, + validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)), + })(originalRequest) - const resHeaders = new Headers(apiResponse.headers) - for (let key in headers) { - resHeaders.append(key, headers[key]) + const resHeaders = new Headers(apiResponse.headers) + for (let key in headers) { + resHeaders.append(key, headers[key]) + } + + return new Response(apiResponse.body, { + status: apiResponse.status, + headers: new Headers(resHeaders), + }) } - - return new Response(apiResponse.body, { - status: apiResponse.status, - headers: new Headers(resHeaders), - }) -} diff --git a/packages/next/src/routes/rest/RouteError.ts b/packages/next/src/routes/rest/RouteError.ts index 72dc5aa2db..76927c1eea 100644 --- a/packages/next/src/routes/rest/RouteError.ts +++ b/packages/next/src/routes/rest/RouteError.ts @@ -66,44 +66,42 @@ export const RouteError = async ({ err: APIError collection?: Collection }) => { - return Response.json(err, { status: 500 }) + const { config, logger } = req.payload + let response = formatErrors(err) + let status = err.status || httpStatus.INTERNAL_SERVER_ERROR - // const { config, logger } = req.payload - // let response = formatErrors(err) - // let status = err.status || httpStatus.INTERNAL_SERVER_ERROR + logger.error(err.stack) - // logger.error(err.stack) + // Internal server errors can contain anything, including potentially sensitive data. + // Therefore, error details will be hidden from the response unless `config.debug` is `true` + if (!config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) { + response = formatErrors(new APIError('Something went wrong.')) + } - // // Internal server errors can contain anything, including potentially sensitive data. - // // Therefore, error details will be hidden from the response unless `config.debug` is `true` - // if (!config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) { - // response = formatErrors(new APIError('Something went wrong.')) - // } + if (config.debug && config.debug === true) { + response.stack = err.stack + } - // if (config.debug && config.debug === true) { - // response.stack = err.stack - // } + if (collection && typeof collection.config.hooks.afterError === 'function') { + ;({ response, status } = (await collection.config.hooks.afterError( + err, + response, + req.context, + collection.config, + )) || { response, status }) + } - // if (collection && typeof collection.config.hooks.afterError === 'function') { - // ;({ response, status } = (await collection.config.hooks.afterError( - // err, - // response, - // req.context, - // collection.config, - // )) || { response, status }) - // } + if (typeof config.hooks.afterError === 'function') { + ;({ response, status } = (await config.hooks.afterError( + err, + response, + req.context, + collection?.config, + )) || { + response, + status, + }) + } - // if (typeof config.hooks.afterError === 'function') { - // ;({ response, status } = (await config.hooks.afterError( - // err, - // response, - // req.context, - // collection?.config, - // )) || { - // response, - // status, - // }) - // } - - // return Response.json(response, { status }) + return Response.json(response, { status }) } diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 1402573aa8..1ac87d67ec 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -145,7 +145,7 @@ const RouteNotFoundResponse = (slug: string[]) => ) export const GET = - (config: Promise) => + (config: Promise | SanitizedConfig) => async (request: Request, { params: { slug } }: { params: { slug: string[] } }) => { const [slug1, slug2, slug3, slug4] = slug let req: PayloadRequest @@ -261,7 +261,7 @@ export const GET = } export const POST = - (config: Promise) => + (config: Promise | SanitizedConfig) => async (request: Request, { params: { slug } }: { params: { slug: string[] } }) => { const [slug1, slug2, slug3, slug4] = slug let req: PayloadRequest @@ -370,7 +370,7 @@ export const POST = } export const DELETE = - (config: Promise) => + (config: Promise | SanitizedConfig) => async (request: Request, { params: { slug } }: { params: { slug: string[] } }) => { const [slug1, slug2] = slug let req: PayloadRequest @@ -427,7 +427,7 @@ export const DELETE = } export const PATCH = - (config: Promise) => + (config: Promise | SanitizedConfig) => async (request: Request, { params: { slug } }: { params: { slug: string[] } }) => { const [slug1, slug2] = slug let req: PayloadRequest diff --git a/packages/next/src/utilities/createPayloadRequest.ts b/packages/next/src/utilities/createPayloadRequest.ts index 9f1d1b469c..663744d0dc 100644 --- a/packages/next/src/utilities/createPayloadRequest.ts +++ b/packages/next/src/utilities/createPayloadRequest.ts @@ -15,7 +15,7 @@ import { getDataAndFile } from './getDataAndFile' type Args = { request: Request - config: Promise + config: Promise | SanitizedConfig params?: { collection: string } diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 5886e3d46c..d61151398a 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -6,10 +6,7 @@ "emitDeclarationOnly": true, "esModuleInterop": true, "outDir": "./dist" /* Specify an output folder for all emitted files. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - "paths": { - "payload-config": ["./src/config.ts"] - } + "rootDir": "./src" /* Specify the root folder within your source files. */ }, "exclude": [ "dist", diff --git a/test/_community/int.spec.ts b/test/_community/int.spec.ts index 92df518dc8..ad90431c44 100644 --- a/test/_community/int.spec.ts +++ b/test/_community/int.spec.ts @@ -1,20 +1,15 @@ import type { Payload } from '../../packages/payload/src' -import { GET as createGET, POST as createPOST } from '../../packages/next/src/routes/rest/index' import { getPayload } from '../../packages/payload/src' import { devUser } from '../credentials' +import { NextRESTClient } from '../helpers/NextRESTClient' import { postsSlug } from './collections/Posts' -import config from './config' +import configPromise from './config' let payload: Payload -let jwt +let token: string +let restClient: NextRESTClient -const GET = createGET(config) -const POST = createPOST(config) - -const headers = { - 'Content-Type': 'application/json', -} const { email, password } = devUser describe('_Community Tests', () => { @@ -22,28 +17,19 @@ describe('_Community Tests', () => { // Boilerplate test setup/teardown // --__--__--__--__--__--__--__--__--__ beforeAll(async () => { - payload = await getPayload({ config }) + payload = await getPayload({ config: configPromise }) + restClient = new NextRESTClient(payload.config) - const req = new Request('http://localhost:3000/api/users/login', { - method: 'POST', - headers: new Headers(headers), - body: JSON.stringify({ - email, - password, - }), - }) + const data = await restClient + .POST('/users/login', { + body: JSON.stringify({ + email, + password, + }), + }) + .then((res) => res.json()) - const data = await POST(req, { - params: { - slug: ['users', 'login'], - }, - }).then((res) => res.json()) - - jwt = data.token - }) - - beforeEach(() => { - jest.resetModules() + token = data.token }) afterAll(async () => { @@ -69,22 +55,16 @@ describe('_Community Tests', () => { }) it('rest API example', async () => { - const req = new Request(`http://localhost:3000/posts`, { - method: 'POST', - headers: new Headers({ - ...headers, - Authorization: `JWT ${jwt}`, - }), - body: JSON.stringify({ - text: 'REST API EXAMPLE', - }), - }) - - const data = await POST(req, { - params: { - slug: ['posts'], - }, - }).then((res) => res.json()) + const data = await restClient + .POST(`/${postsSlug}`, { + body: JSON.stringify({ + text: 'REST API EXAMPLE', + }), + headers: { + Authorization: `JWT ${token}`, + }, + }) + .then((res) => res.json()) expect(data.doc.text).toEqual('REST API EXAMPLE') }) diff --git a/test/auth/config.ts b/test/auth/config.ts index b7223a19b2..1c6bcc2348 100644 --- a/test/auth/config.ts +++ b/test/auth/config.ts @@ -158,16 +158,16 @@ export default buildConfigWithDefaults({ label: 'Custom', type: 'text', }, - { - name: 'authDebug', - label: 'Auth Debug', - type: 'ui', - admin: { - components: { - Field: AuthDebug, - }, - }, - }, + // { + // name: 'authDebug', + // label: 'Auth Debug', + // type: 'ui', + // admin: { + // components: { + // Field: AuthDebug, + // }, + // }, + // }, ], }, { diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index 048e43f54d..1e105a0bdd 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -1,37 +1,24 @@ -import { GraphQLClient } from 'graphql-request' import jwtDecode from 'jwt-decode' import type { Payload } from '../../packages/payload/src' import type { User } from '../../packages/payload/src/auth' -import configPromise from '../collections-graphql/config' +import { getPayload } from '../../packages/payload/src' import { devUser } from '../credentials' -import { initPayloadTest } from '../helpers/configHelpers' +import { NextRESTClient } from '../helpers/NextRESTClient' +import configPromise from './config' import { namedSaveToJWTValue, saveToJWTKey, slug } from './shared' -require('isomorphic-fetch') - let apiUrl -let client: GraphQLClient +let restClient: NextRESTClient let payload: Payload -const headers = { - 'Content-Type': 'application/json', -} - const { email, password } = devUser describe('Auth', () => { beforeAll(async () => { - const { serverURL, payload: payloadClient } = await initPayloadTest({ - __dirname, - init: { local: false }, - }) - payload = payloadClient - apiUrl = `${serverURL}/api` - const config = await configPromise - const url = `${serverURL}${config.routes.api}${config.routes.graphQL}` - client = new GraphQLClient(url) + payload = await getPayload({ config: configPromise, disableOnInit: true }) + restClient = new NextRESTClient(payload.config) }) afterAll(async () => { @@ -40,27 +27,28 @@ describe('Auth', () => { } }) - beforeEach(() => { - jest.resetModules() - }) - describe('GraphQL - admin user', () => { let token let user beforeAll(async () => { - // language=graphQL - const query = `mutation { - loginUser(email: "${devUser.email}", password: "${devUser.password}") { + const { data } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ + query: `mutation { + loginUser(email: "${devUser.email}", password: "${devUser.password}") { token user { id email } - } - }` - const response = await client.request(query) - user = response.loginUser.user - token = response.loginUser.token + } + }`, + }), + }) + .then((res) => res.json()) + + user = data.loginUser.user + token = data.loginUser.token }) it('should login', async () => { @@ -83,37 +71,32 @@ describe('Auth', () => { describe('REST - admin user', () => { beforeAll(async () => { - await fetch(`${apiUrl}/${slug}/first-register`, { + const u = await restClient.POST(`/${slug}/first-register`, { body: JSON.stringify({ email, password, }), - headers, - method: 'post', }) + const t = u }) it('should prevent registering a new first user', async () => { - const response = await fetch(`${apiUrl}/${slug}/first-register`, { + const response = await restClient.POST(`/${slug}/first-register`, { body: JSON.stringify({ - email: 'thisuser@shouldbeprevented.com', - password: 'get-out', + email, + password, }), - headers, - method: 'post', }) expect(response.status).toBe(403) }) it('should login a user successfully', async () => { - const response = await fetch(`${apiUrl}/${slug}/login`, { + const response = await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email, password, }), - headers, - method: 'post', }) const data = await response.json() @@ -127,13 +110,11 @@ describe('Auth', () => { let loggedInUser: User | undefined beforeAll(async () => { - const response = await fetch(`${apiUrl}/${slug}/login`, { + const response = await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email, password, }), - headers, - method: 'post', }) const data = await response.json() @@ -155,9 +136,8 @@ describe('Auth', () => { }) it('should return a logged in user from /me', async () => { - const response = await fetch(`${apiUrl}/${slug}/me`, { + const response = await restClient.GET(`/${slug}/me`, { headers: { - ...headers, Authorization: `JWT ${token}`, }, }) @@ -218,9 +198,8 @@ describe('Auth', () => { }, }) - const response = await fetch(`${apiUrl}/${slug}/me`, { + const response = await restClient.GET(`/${slug}/me`, { headers: { - ...headers, Authorization: `${slug} API-Key ${user?.apiKey}`, }, }) @@ -233,11 +212,10 @@ describe('Auth', () => { }) it('should refresh a token and reset its expiration', async () => { - const response = await fetch(`${apiUrl}/${slug}/refresh-token`, { + const response = await restClient.POST(`/${slug}/refresh-token`, { headers: { Authorization: `JWT ${token}`, }, - method: 'post', }) const data = await response.json() @@ -257,11 +235,10 @@ describe('Auth', () => { }, }) - const response = await fetch(`${apiUrl}/${slug}/refresh-token`, { + const response = await restClient.POST(`/${slug}/refresh-token`, { headers: { Authorization: `JWT ${token}`, }, - method: 'post', }) const data = await response.json() @@ -271,7 +248,7 @@ describe('Auth', () => { }) it('should allow a user to be created', async () => { - const response = await fetch(`${apiUrl}/${slug}`, { + const response = await restClient.POST(`/${slug}`, { body: JSON.stringify({ email: 'name@test.com', password, @@ -279,9 +256,7 @@ describe('Auth', () => { }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) const data = await response.json() @@ -299,7 +274,7 @@ describe('Auth', () => { it('should allow verification of a user', async () => { const emailToVerify = 'verify@me.com' - const response = await fetch(`${apiUrl}/public-users`, { + const response = await restClient.POST(`/public-users`, { body: JSON.stringify({ email: emailToVerify, password, @@ -307,9 +282,7 @@ describe('Auth', () => { }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) expect(response.status).toBe(201) @@ -330,14 +303,8 @@ describe('Auth', () => { expect(_verified).toBe(false) expect(_verificationToken).toBeDefined() - const verificationResponse = await fetch( - `${apiUrl}/public-users/verify/${_verificationToken}`, - { - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', - }, + const verificationResponse = await restClient.POST( + `/public-users/verify/${_verificationToken}`, ) expect(verificationResponse.status).toBe(200) @@ -365,15 +332,13 @@ describe('Auth', () => { let data beforeAll(async () => { - const response = await fetch(`${apiUrl}/payload-preferences/${key}`, { + const response = await restClient.POST(`/${slug}/payload-preferences/${key}`, { body: JSON.stringify({ value: { property }, }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) data = await response.json() }) @@ -384,12 +349,10 @@ describe('Auth', () => { }) it('should read', async () => { - const response = await fetch(`${apiUrl}/payload-preferences/${key}`, { + const response = await restClient.GET(`/${slug}/payload-preferences/${key}`, { headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'get', }) data = await response.json() expect(data.key).toStrictEqual(key) @@ -397,15 +360,13 @@ describe('Auth', () => { }) it('should update', async () => { - const response = await fetch(`${apiUrl}/payload-preferences/${key}`, { + const response = await restClient.POST(`/${slug}/payload-preferences/${key}`, { body: JSON.stringify({ value: { property: 'updated', property2: 'test' }, }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) data = await response.json() @@ -426,14 +387,11 @@ describe('Auth', () => { }) it('should delete', async () => { - const response = await fetch(`${apiUrl}/payload-preferences/${key}`, { + const response = await restClient.DELETE(`/${slug}/payload-preferences/${key}`, { headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'delete', }) - data = await response.json() const result = await payload.find({ @@ -452,43 +410,34 @@ describe('Auth', () => { const userEmail = 'lock@me.com' const tryLogin = async () => { - await fetch(`${apiUrl}/${slug}/login`, { + await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email: userEmail, password: 'bad', }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', }) - // expect(loginRes.status).toEqual(401); } beforeAll(async () => { - const response = await fetch(`${apiUrl}/${slug}/login`, { + const response = await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email, password, }), - headers, - method: 'post', }) const data = await response.json() token = data.token // New user to lock - await fetch(`${apiUrl}/${slug}`, { + await restClient.POST(`/${slug}`, { body: JSON.stringify({ email: userEmail, password, }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) }) @@ -531,16 +480,14 @@ describe('Auth', () => { }) // login - await fetch(`${apiUrl}/${slug}/login`, { + await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email: userEmail, password, }), headers: { Authorization: `JWT ${token}`, - 'Content-Type': 'application/json', }, - method: 'post', }) const userResult = await payload.find({ @@ -564,16 +511,11 @@ describe('Auth', () => { it('should allow forgot-password by email', async () => { // TODO: Spy on payload sendEmail function - const response = await fetch(`${apiUrl}/${slug}/forgot-password`, { + const response = await restClient.POST(`/${slug}/forgot-password`, { body: JSON.stringify({ email, }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'post', }) - // expect(mailSpy).toHaveBeenCalled(); expect(response.status).toBe(200) @@ -613,21 +555,22 @@ describe('Auth', () => { }, }) - const response = await fetch(`${apiUrl}/${slug}/login`, { + const response = await restClient.POST(`/${slug}/login`, { body: JSON.stringify({ email: 'insecure@me.com', password: 'test', }), - headers, - method: 'post', }) const data = await response.json() - const adminMe = await fetch(`${apiUrl}/${slug}/me`, { - headers: { - Authorization: `JWT ${data.token}`, - }, - }).then((res) => res.json()) + const adminMe = await restClient + .GET(`/${slug}/me`, { + headers: { + Authorization: `JWT ${data.token}`, + }, + }) + .then((res) => res.json()) + expect(adminMe.user.adminOnlyField).toEqual('admin secret') await payload.update({ @@ -638,21 +581,21 @@ describe('Auth', () => { }, }) - const editorMe = await fetch(`${apiUrl}/${slug}/me`, { - headers: { - Authorization: `JWT ${adminMe?.token}`, - }, - }).then((res) => res.json()) + const editorMe = await restClient + .GET(`/${slug}/me`, { + headers: { + Authorization: `JWT ${data.token}`, + }, + }) + .then((res) => res.json()) expect(editorMe.user.adminOnlyField).toBeUndefined() }) it('should not allow refreshing an invalid token', async () => { - const response = await fetch(`${apiUrl}/${slug}/refresh-token`, { + const response = await restClient.POST(`/${slug}/refresh-token`, { body: JSON.stringify({ token: 'INVALID', }), - headers, - method: 'post', }) const data = await response.json() @@ -678,19 +621,19 @@ describe('Auth', () => { const [user1, user2] = usersQuery.docs - const success = await fetch(`${apiUrl}/api-keys/${user2.id}`, { - headers: { - Authorization: `api-keys API-Key ${user2.apiKey}`, - 'Content-Type': 'application/json', - }, - }).then((res) => res.json()) + const success = await restClient + .GET(`/api-keys/${user2.id}`, { + headers: { + Authorization: `api-keys API-Key ${user2.apiKey}`, + }, + }) + .then((res) => res.json()) expect(success.apiKey).toStrictEqual(user2.apiKey) - const fail = await fetch(`${apiUrl}/api-keys/${user1.id}`, { + const fail = await restClient.GET(`/api-keys/${user1.id}`, { headers: { Authorization: `api-keys API-Key ${user2.apiKey}`, - 'Content-Type': 'application/json', }, }) diff --git a/test/helpers/NextRESTClient.ts b/test/helpers/NextRESTClient.ts new file mode 100644 index 0000000000..f9f95482b9 --- /dev/null +++ b/test/helpers/NextRESTClient.ts @@ -0,0 +1,94 @@ +import type { SanitizedConfig } from '../../packages/payload/types' + +import { GRAPHQL_POST as createGraphqlPOST } from '../../packages/next/src/routes/graphql' +import { + DELETE as createDELETE, + GET as createGET, + PATCH as createPATCH, + POST as createPOST, +} from '../../packages/next/src/routes/rest' + +type ValidPath = `/${string}` + +export class NextRESTClient { + private _DELETE: (request: Request, args: { params: { slug: string[] } }) => Promise + + private _GET: (request: Request, args: { params: { slug: string[] } }) => Promise + + private _GRAPHQL_POST: (request: Request) => Promise + + private _PATCH: (request: Request, args: { params: { slug: string[] } }) => Promise + + private _POST: (request: Request, args: { params: { slug: string[] } }) => Promise + + private readonly config: SanitizedConfig + + serverURL: string = 'http://localhost:3000' + + constructor(config: SanitizedConfig) { + this.config = config + if (config?.serverURL) this.serverURL = config.serverURL + this._GET = createGET(config) + this._POST = createPOST(config) + this._DELETE = createDELETE(config) + this._PATCH = createPATCH(config) + this._GRAPHQL_POST = createGraphqlPOST(config) + } + + private generateRequestParts(path: string): { + slug: string[] + url: string + } { + const safePath = path.slice(1) + const slug = safePath.split('/') + const url = `${this.serverURL}${this.config.routes.api}/${safePath}` + return { + url, + slug, + } + } + + async DELETE(path: ValidPath, options: RequestInit): Promise { + const { url, slug } = this.generateRequestParts(path) + const request = new Request(url, { ...options, method: 'DELETE' }) + return this._DELETE(request, { params: { slug } }) + } + + async GET(path: ValidPath, options?: Omit): Promise { + const { url, slug } = this.generateRequestParts(path) + const request = new Request(url, { ...options, method: 'GET' }) + return this._GET(request, { params: { slug } }) + } + + async GRAPHQL_POST(options: RequestInit): Promise { + const request = new Request(`${this.serverURL}${this.config.routes.graphQL}`, { + ...options, + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + ...(options?.headers || {}), + }), + }) + return this._GRAPHQL_POST(request) + } + + async PATCH(path: ValidPath, options: RequestInit): Promise { + const { url, slug } = this.generateRequestParts(path) + const request = new Request(url, { ...options, method: 'PATCH' }) + return this._PATCH(request, { params: { slug } }) + } + + async POST(path: ValidPath, options?: RequestInit): Promise { + const { url, slug } = this.generateRequestParts(path) + + const request = new Request(url, { + ...options, + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + ...(options?.headers || {}), + }), + }) + return this._POST(request, { params: { slug } }) + } +} diff --git a/tsconfig.json b/tsconfig.json index caa2cf5102..2b08b98b6c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,11 +11,7 @@ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "noEmit": true /* Do not emit outputs. */, /* Concatenate and emit output to single file. */ "outDir": "./dist" /* Redirect output structure to the directory. */, @@ -24,11 +20,7 @@ "skipLibCheck": true /* Skip type checking of declaration files. */, "sourceMap": true, "strict": false /* Enable all strict type-checking options. */, - "types": [ - "jest", - "node", - "@types/jest" - ], + "types": ["jest", "node", "@types/jest"], "incremental": true, "isolatedModules": true, "plugins": [ @@ -37,47 +29,19 @@ } ], "paths": { - "payload": [ - "./packages/payload/src" - ], - "payload/*": [ - "./packages/payload/src/exports/*" - ], - "@payloadcms/db-mongodb": [ - "./packages/db-mongodb/src" - ], - "@payloadcms/richtext-lexical": [ - "./packages/richtext-lexical/src" - ], - "@payloadcms/ui/*": [ - "./packages/ui/src/exports/*" - ], - "@payloadcms/translations": [ - "./packages/translations/src/exports/index.ts" - ], - "@payloadcms/translations/client": [ - "./packages/translations/src/all" - ], - "@payloadcms/translations/api": [ - "./packages/translations/src/all" - ], - "@payloadcms/next/*": [ - "./packages/next/src/*" - ], - "@payloadcms/graphql": [ - "./packages/graphql/src" - ], - "payload-config": [ - "./test/_community/config.ts" - ] + "payload": ["./packages/payload/src"], + "payload/*": ["./packages/payload/src/exports/*"], + "@payloadcms/db-mongodb": ["./packages/db-mongodb/src"], + "@payloadcms/richtext-lexical": ["./packages/richtext-lexical/src"], + "@payloadcms/ui/*": ["./packages/ui/src/exports/*"], + "@payloadcms/translations": ["./packages/translations/src/exports/index.ts"], + "@payloadcms/translations/client": ["./packages/translations/src/all"], + "@payloadcms/translations/api": ["./packages/translations/src/all"], + "@payloadcms/next/*": ["./packages/next/src/*"], + "@payloadcms/graphql": ["./packages/graphql/src"] } }, - "exclude": [ - "dist", - "build", - "temp", - "node_modules" - ], + "exclude": ["dist", "build", "temp", "node_modules"], /* Like tsconfig.build.json, but includes test directory and doesnt emit anything */ "composite": true, // Required for references to work "references": [ @@ -140,10 +104,5 @@ "path": "./packages/ui" } ], - "include": [ - "next-env.d.ts", - ".next/types/**/*.ts", - "**/*.ts", - "**/*.tsx" - ] + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"] }