diff --git a/.eslintrc.js b/.eslintrc.js index 11ae795b0..42e5be8ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,6 +40,12 @@ module.exports = { ], }, }, + { + files: ['*.spec.ts'], + rules: { + '@typescript-eslint/no-use-before-define': 'off', + }, + }, ], rules: { 'no-sparse-arrays': 'off', @@ -63,6 +69,5 @@ module.exports = { tsx: 'never', }, ], - 'operator-linbreak': 'off', }, }; diff --git a/demo/payload.config.ts b/demo/payload.config.ts index e2e906b23..2ff4bdfce 100644 --- a/demo/payload.config.ts +++ b/demo/payload.config.ts @@ -166,7 +166,9 @@ export default buildConfig({ // indexSortableFields: true, hooks: { afterError: (err) => { - console.error('global error config handler', err); + if (process.env.DISABLE_LOGGING !== 'true') { + console.error('global error config handler', err); + } }, }, upload: { diff --git a/src/collections/operations/local/local.spec.ts b/src/collections/operations/local/local.spec.ts index be905729d..e047e766a 100644 --- a/src/collections/operations/local/local.spec.ts +++ b/src/collections/operations/local/local.spec.ts @@ -14,10 +14,13 @@ describe('Collections - Local', () => { describe('Create', () => { it('should allow an upload-enabled file to be created and uploaded', async () => { const alt = 'Alt Text Here'; - const filePath = path.resolve(__dirname, '../../../admin/assets/images/generic-block-image.svg'); + const filePath = path.resolve( + __dirname, + '../../../admin/assets/images/generic-block-image.svg', + ); const { size } = fs.statSync(filePath); - const result = await payload.create({ + const result: Media = await payload.create({ collection: 'media', data: { alt, @@ -37,7 +40,7 @@ describe('Collections - Local', () => { it('should allow an upload-enabled file to be re-uploaded and alt-text to be changed.', async () => { const newAltText = 'New Alt Text Here'; - const result = await payload.update({ + const result: Media = await payload.update({ collection: 'media', id: createdMediaID, data: { @@ -115,7 +118,9 @@ describe('Collections - Local', () => { showHiddenFields: true, }); - expect(relationshipBWithHiddenNestedField.post[0].demoHiddenField).toStrictEqual(demoHiddenField); + expect(relationshipBWithHiddenNestedField.post[0].demoHiddenField).toStrictEqual( + demoHiddenField, + ); }); describe('Find', () => { const title = 'local-find'; @@ -139,9 +144,11 @@ describe('Collections - Local', () => { { blockType: 'richTextBlock', blockName: 'Test Block Name', - content: [{ - children: [{ text: 'english' }], - }], + content: [ + { + children: [{ text: 'english' }], + }, + ], }, ], }; @@ -185,3 +192,27 @@ describe('Collections - Local', () => { }); }); }); + +interface ImageSize { + url?: string; + width?: number; + height?: number; + mimeType?: string; + filesize?: number; + filename?: string; +} + +interface Media { + id?: string; + filename?: string; + filesize?: number; + width?: number; + height?: number; + sizes?: { + maintainedAspectRatio?: ImageSize; + tablet?: ImageSize; + mobile?: ImageSize; + icon?: ImageSize; + }; + alt: string; +} diff --git a/src/collections/tests/uploads.spec.ts b/src/collections/tests/uploads.spec.ts index 2bfc93b0b..34a0c46df 100644 --- a/src/collections/tests/uploads.spec.ts +++ b/src/collections/tests/uploads.spec.ts @@ -2,10 +2,12 @@ import fs from 'fs'; import path from 'path'; import FormData from 'form-data'; import { GraphQLClient } from 'graphql-request'; +import { promisify } from 'util'; import getConfig from '../../config/load'; -import fileExists from '../../../tests/api/utils/fileExists'; import { email, password } from '../../mongoose/testCredentials'; +const stat = promisify(fs.stat); + require('isomorphic-fetch'); const config = getConfig(); @@ -59,11 +61,14 @@ describe('Collections - Uploads', () => { describe('create', () => { it('creates', async () => { const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png')), + ); formData.append('alt', 'test media'); formData.append('locale', 'en'); const response = await fetch(`${api}/media`, { - body: formData, + body: formData as unknown as BodyInit, headers, method: 'post', }); @@ -113,12 +118,15 @@ describe('Collections - Uploads', () => { it('creates media without storing a file', async () => { const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png')), + ); formData.append('alt', 'test media'); formData.append('locale', 'en'); const response = await fetch(`${api}/unstored-media`, { - body: formData, + body: formData as unknown as BodyInit, headers, method: 'post', }); @@ -150,12 +158,15 @@ describe('Collections - Uploads', () => { it('creates with same name', async () => { const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/samename.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/samename.png')), + ); formData.append('alt', 'test media'); formData.append('locale', 'en'); const firstResponse = await fetch(`${api}/media`, { - body: formData, + body: formData as unknown as BodyInit, headers, method: 'post', }); @@ -163,12 +174,15 @@ describe('Collections - Uploads', () => { expect(firstResponse.status).toBe(201); const sameForm = new FormData(); - sameForm.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/samename.png'))); + sameForm.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/samename.png')), + ); sameForm.append('alt', 'test media'); sameForm.append('locale', 'en'); const response = await fetch(`${api}/media`, { - body: sameForm, + body: sameForm as unknown as BodyInit, headers, method: 'post', }); @@ -211,15 +225,16 @@ describe('Collections - Uploads', () => { it('update', async () => { const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/update.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/update.png')), + ); formData.append('alt', 'test media'); formData.append('locale', 'en'); const response = await fetch(`${api}/media`, { - body: formData, - headers: { - Authorization: `JWT ${token}`, - }, + body: formData as unknown as BodyInit, + headers, method: 'post', }); @@ -233,10 +248,8 @@ describe('Collections - Uploads', () => { updateFormData.append('filename', data.doc.filename); updateFormData.append('alt', newAlt); const updateResponse = await fetch(`${api}/media/${data.doc.id}`, { - body: updateFormData, - headers: { - Authorization: `JWT ${token}`, - }, + body: updateFormData as unknown as BodyInit, + headers, method: 'put', }); @@ -280,15 +293,16 @@ describe('Collections - Uploads', () => { it('delete', async () => { const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/delete.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/delete.png')), + ); formData.append('alt', 'test media'); formData.append('locale', 'en'); const createResponse = await fetch(`${api}/media`, { - body: formData, - headers: { - Authorization: `JWT ${token}`, - }, + body: formData as unknown as BodyInit, + headers, method: 'post', }); @@ -297,9 +311,7 @@ describe('Collections - Uploads', () => { const docId = createData.doc.id; const response = await fetch(`${api}/media/${docId}`, { - headers: { - Authorization: `JWT ${token}`, - }, + headers, method: 'delete', }); @@ -318,15 +330,20 @@ describe('Collections - Uploads', () => { let image; const alt = 'alt text'; beforeAll(async (done) => { - client = new GraphQLClient(`${api}${config.routes.graphQL}`, { headers: { Authorization: `JWT ${token}` } }); + client = new GraphQLClient(`${api}${config.routes.graphQL}`, { + headers: { Authorization: `JWT ${token}` }, + }); // create media using REST const formData = new FormData(); - formData.append('file', fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png'))); + formData.append( + 'file', + fs.createReadStream(path.join(__dirname, '../../..', 'tests/api/assets/image.png')), + ); formData.append('alt', alt); formData.append('locale', 'en'); const mediaResponse = await fetch(`${api}/media`, { - body: formData, + body: formData as unknown as BodyInit, headers, method: 'post', }); @@ -364,3 +381,12 @@ describe('Collections - Uploads', () => { }); }); }); + +async function fileExists(fileName: string): Promise { + try { + await stat(fileName); + return true; + } catch (err) { + return false; + } +} diff --git a/src/express/middleware/errorHandler.spec.ts b/src/express/middleware/errorHandler.spec.ts index be76a5506..822567ba3 100644 --- a/src/express/middleware/errorHandler.spec.ts +++ b/src/express/middleware/errorHandler.spec.ts @@ -1,120 +1,112 @@ +import { Response } from 'express'; import Logger from '../../utilities/logger'; import errorHandler from './errorHandler'; import { APIError } from '../../errors'; +import { PayloadRequest } from '../types'; +import { SanitizedConfig } from '../../config/types'; const logger = Logger(); const testError = new APIError('test error', 503); -const mockResponse = () => { - const res = { - status: jest.fn(), - send: jest.fn(), - }; - - jest.spyOn(res, 'status').mockImplementation() - .mockReturnValue(res); - - jest.spyOn(res, 'send').mockImplementation() - .mockReturnValue(res); - return res; -}; - -const mockRequest = async () => { - const req = {}; - req.collection = { - config: { - hooks: {}, - }, - }; - req.collection.config.hooks.afterError = await jest.fn(); - return req; -}; - describe('errorHandler', () => { - let res; - let req; - beforeAll(async (done) => { - res = mockResponse(); - req = await mockRequest(); - done(); - }); + const res = generateResponse(); + const next = jest.fn(); + const req = generateRequest() as PayloadRequest; it('should send the response with the error', async () => { - const handler = errorHandler({ debug: true, hooks: {} }, logger); - await handler(testError, req, res); - expect(res.send) - .toHaveBeenCalledWith( - expect.objectContaining({ errors: [{ message: 'test error' }] }), - ); + const handler = errorHandler(generateConfig({ debug: true }), logger); + await handler(testError, req, res, next); + expect(res.send).toHaveBeenCalledWith( + expect.objectContaining({ errors: [{ message: 'test error' }] }), + ); }); it('should include stack trace when config debug is on', async () => { - const handler = errorHandler({ debug: true, hooks: {} }, logger); - await handler(testError, req, res); - expect(res.send) - .toHaveBeenCalledWith( - expect.objectContaining({ stack: expect.any(String) }), - ); + const handler = errorHandler(generateConfig({ debug: true }), logger); + await handler(testError, req, res, next); + expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ stack: expect.any(String) })); }); it('should not include stack trace when config debug is not set', async () => { - const handler = errorHandler({ hooks: {} }, logger); - await handler(testError, req, res); - expect(res.send) - .toHaveBeenCalledWith( - expect.not.objectContaining({ stack: expect.any(String) }), - ); + const handler = errorHandler(generateConfig({ debug: undefined }), logger); + await handler(testError, req, res, next); + expect(res.send).toHaveBeenCalledWith( + expect.not.objectContaining({ stack: expect.any(String) }), + ); }); it('should not include stack trace when config debug is false', async () => { - const handler = errorHandler({ debug: false, hooks: {} }, logger); - await handler(testError, req, res); - expect(res.send) - .toHaveBeenCalledWith( - expect.not.objectContaining({ stack: expect.any(String) }), - ); + const handler = errorHandler(generateConfig({ debug: false }), logger); + await handler(testError, req, res, next); + expect(res.send).toHaveBeenCalledWith( + expect.not.objectContaining({ stack: expect.any(String) }), + ); }); it('should show the status code when given an error with a code', async () => { - const handler = errorHandler({ debug: false, hooks: {} }, logger); - await handler(testError, req, res); - expect(res.status) - .toHaveBeenCalledWith( - 503, - ); + const handler = errorHandler(generateConfig(), logger); + await handler(testError, req, res, next); + expect(res.status).toHaveBeenCalledWith(503); }); it('should default to 500 when an error does not have a status code', async () => { - const handler = errorHandler({ debug: false, hooks: {} }, logger); + const handler = errorHandler(generateConfig(), logger); testError.status = undefined; - await handler(testError, req, res); - expect(res.status) - .toHaveBeenCalledWith( - 500, - ); + await handler(testError, req, res, next); + expect(res.status).toHaveBeenCalledWith(500); }); it('should call payload config afterError hook', async () => { const afterError = jest.fn(); - const handler = errorHandler({ - debug: false, - hooks: { afterError }, - }, logger); - await handler(testError, req, res); + const handler = errorHandler( + generateConfig({ + hooks: { afterError }, + }), + logger, + ); + await handler(testError, req, res, next); expect(afterError) // eslint-disable-next-line jest/prefer-called-with .toHaveBeenCalled(); }); it('should call collection config afterError hook', async () => { - const handler = errorHandler({ - debug: false, - hooks: {}, - }, logger); - await handler(testError, req, res); + const handler = errorHandler(generateConfig(), logger); + await handler(testError, req, res, next); expect(req.collection.config.hooks.afterError) // eslint-disable-next-line jest/prefer-called-with .toHaveBeenCalled(); }); }); + +function generateResponse() { + const res = { + status: jest.fn(), + send: jest.fn(), + }; + + jest.spyOn(res, 'status').mockImplementation().mockReturnValue(res); + jest.spyOn(res, 'send').mockImplementation().mockReturnValue(res); + return res as unknown as Response; +} + +function generateRequest(): PayloadRequest { + return { + collection: { + config: { + hooks: { + afterError: jest.fn(), + }, + }, + }, + } as unknown as PayloadRequest; +} + +function generateConfig(overrides?: Partial): SanitizedConfig { + return { + debug: false, + hooks: { afterError: jest.fn() }, + ...overrides, + } as unknown as SanitizedConfig; +} diff --git a/tests/api/globalSetup.ts b/tests/api/globalSetup.ts index 843b4f0b4..d029b2f5e 100644 --- a/tests/api/globalSetup.ts +++ b/tests/api/globalSetup.ts @@ -1,9 +1,12 @@ import path from 'path'; import fs from 'fs'; import { email, password } from '../../src/mongoose/testCredentials'; -import fileExists from './utils/fileExists'; +import { fileExists } from './utils/fileExists'; import loadConfig from '../../src/config/load'; +process.env.PAYLOAD_CONFIG_PATH = 'demo/payload.config.ts'; +process.env.DISABLE_LOGGING = 'true'; + require('isomorphic-fetch'); require('../../demo/server'); diff --git a/tests/api/utils/fileExists.js b/tests/api/utils/fileExists.js deleted file mode 100644 index 50b95321a..000000000 --- a/tests/api/utils/fileExists.js +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require('fs'); -const { promisify } = require('util'); - -const stat = promisify(fs.stat); - -const fileExists = async (fileName) => { - try { - await stat(fileName); - return true; - } catch (err) { - return false; - } -}; - -module.exports = fileExists; diff --git a/tests/api/utils/fileExists.ts b/tests/api/utils/fileExists.ts new file mode 100644 index 000000000..2da36c8fa --- /dev/null +++ b/tests/api/utils/fileExists.ts @@ -0,0 +1,13 @@ +import fs from 'fs'; +import { promisify } from 'util'; + +const stat = promisify(fs.stat); + +export const fileExists = async (fileName: string): Promise => { + try { + await stat(fileName); + return true; + } catch (err) { + return false; + } +}; diff --git a/tsconfig.json b/tsconfig.json index 32d1661c1..cb2242617 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,7 @@ "build", "tests", "**/*.spec.js", + "**/*.spec.ts", "node_modules", ".eslintrc.js" ]