chore: partial passing upload int tests

This commit is contained in:
Jarrod Flesch
2024-02-19 10:03:35 -05:00
parent 1c9ba5b512
commit 19a4a99e76
9 changed files with 199 additions and 140 deletions

View File

@@ -66,6 +66,16 @@ export const RouteError = async ({
err: APIError
collection?: Collection
}) => {
if (!req?.payload) {
return Response.json(
{
message: err.message,
stack: err.stack,
},
{ status: httpStatus.INTERNAL_SERVER_ERROR },
)
}
const { config, logger } = req.payload
let response = formatErrors(err)
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR

View File

@@ -28,8 +28,7 @@ export const deleteAssociatedFiles: (args: Args) => Promise<void> = async ({
}) => {
if (!collectionConfig.upload) return
if (overrideDelete || files.length > 0) {
const staticPath = path.resolve(collectionConfig.upload.staticDir)
// const staticPath = path.resolve(config.paths.configDir, collectionConfig.upload.staticDir)
const staticPath = path.resolve(config.paths.configDir, collectionConfig.upload.staticDir)
const fileToDelete = `${staticPath}/${doc.filename}`

View File

@@ -61,8 +61,7 @@ export const generateFileData = async <T>({
let staticPath = staticDir
if (staticDir.indexOf('/') !== 0) {
staticPath = path.resolve(staticDir)
// staticPath = path.resolve(config.paths.configDir, staticDir)
staticPath = path.resolve(config.paths.configDir, staticDir)
}
if (!file && uploadEdits && data) {

View File

@@ -15,7 +15,9 @@ import {
import { devUser } from '../credentials'
type ValidPath = `/${string}`
type RequestQuery = {
type RequestOptions = {
auth?: boolean
file?: boolean
query?: {
depth?: number
fallbackLocale?: string
@@ -27,7 +29,7 @@ type RequestQuery = {
}
}
function generateQueryString(query: RequestQuery['query'], params: ParsedQs): string {
function generateQueryString(query: RequestOptions['query'], params: ParsedQs): string {
return QueryString.stringify(
{
...(params || {}),
@@ -52,6 +54,8 @@ export class NextRESTClient {
private readonly config: SanitizedConfig
private token: string
serverURL: string = 'http://localhost:3000'
constructor(config: SanitizedConfig) {
@@ -64,6 +68,22 @@ export class NextRESTClient {
this._GRAPHQL_POST = createGraphqlPOST(config)
}
private buildHeaders(options: RequestInit & RequestOptions): Headers {
const defaultHeaders = {
'Content-Type': 'application/json',
}
const headers = new Headers({
...(options?.file ? {} : defaultHeaders),
...(options?.headers || {}),
})
if (options.auth !== false && this.token) {
headers.set('Authorization', `JWT ${this.token}`)
}
return headers
}
private generateRequestParts(path: ValidPath): {
params?: ParsedQs
slug: string[]
@@ -79,7 +99,7 @@ export class NextRESTClient {
}
}
async DELETE(path: ValidPath, options: RequestInit & RequestQuery = {}): Promise<Response> {
async DELETE(path: ValidPath, options: RequestInit & RequestOptions = {}): Promise<Response> {
const { url, slug, params } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
const queryParams = generateQueryString(query, params)
@@ -87,17 +107,14 @@ export class NextRESTClient {
const request = new Request(`${url}${queryParams}`, {
...rest,
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...(options?.headers || {}),
},
headers: this.buildHeaders(options),
})
return this._DELETE(request, { params: { slug } })
}
async GET(
path: ValidPath,
options: Omit<RequestInit, 'body'> & RequestQuery = {},
options: Omit<RequestInit, 'body'> & RequestOptions = {},
): Promise<Response> {
const { url, slug, params } = this.generateRequestParts(path)
const { query, ...rest } = options || {}
@@ -106,15 +123,12 @@ export class NextRESTClient {
const request = new Request(`${url}${queryParams}`, {
...rest,
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json',
...(options?.headers || {}),
}),
headers: this.buildHeaders(options),
})
return this._GET(request, { params: { slug } })
}
async GRAPHQL_POST(options: RequestInit & RequestQuery): Promise<Response> {
async GRAPHQL_POST(options: RequestInit & RequestOptions): Promise<Response> {
const { query, ...rest } = options
const queryParams = generateQueryString(query, {})
const request = new Request(
@@ -122,16 +136,13 @@ export class NextRESTClient {
{
...rest,
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
...(options?.headers || {}),
}),
headers: this.buildHeaders(options),
},
)
return this._GRAPHQL_POST(request)
}
async PATCH(path: ValidPath, options: RequestInit & RequestQuery): Promise<Response> {
async PATCH(path: ValidPath, options: RequestInit & RequestOptions): Promise<Response> {
const { url, slug, params } = this.generateRequestParts(path)
const { query, ...rest } = options
const queryParams = generateQueryString(query, params)
@@ -139,25 +150,19 @@ export class NextRESTClient {
const request = new Request(`${url}${queryParams}`, {
...rest,
method: 'PATCH',
headers: new Headers({
'Content-Type': 'application/json',
...(options?.headers || {}),
}),
headers: this.buildHeaders(options),
})
return this._PATCH(request, { params: { slug } })
}
async POST(path: ValidPath, options: RequestInit = {}): Promise<Response> {
async POST(path: ValidPath, options: RequestInit & { file?: boolean } = {}): Promise<Response> {
const { url, slug, params } = this.generateRequestParts(path)
const queryParams = generateQueryString({}, params)
const request = new Request(`${url}${queryParams}`, {
...options,
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
...(options?.headers || {}),
}),
headers: this.buildHeaders(options),
})
return this._POST(request, { params: { slug } })
}
@@ -171,11 +176,15 @@ export class NextRESTClient {
password: string
}
slug: string
}): Promise<Response> {
return this.POST(`/${slug}/login`, {
}): Promise<string> {
this.token = await this.POST(`/${slug}/login`, {
body: JSON.stringify(
credentials ? { ...credentials } : { email: devUser.email, password: devUser.password },
),
})
.then((res) => res.json())
.then((data) => data.token)
return this.token
}
}

View File

@@ -29,7 +29,6 @@ import {
let restClient: NextRESTClient
let payload: Payload
let token: string
type EasierChained = { id: string; relation: EasierChained }
@@ -38,7 +37,7 @@ describe('Relationships', () => {
const config = await startMemoryDB(configPromise)
payload = await getPayload({ config })
restClient = new NextRESTClient(payload.config)
;({ token } = await restClient.login({ slug: usersSlug }).then((res) => res.json()))
await restClient.login({ slug: usersSlug })
})
afterAll(async () => {
@@ -153,18 +152,14 @@ describe('Relationships', () => {
})
it('should prevent an unauthorized population of strict access', async () => {
const doc = await restClient.GET(`/${slug}/${post.id}`).then((res) => res.json())
const doc = await restClient
.GET(`/${slug}/${post.id}`, { auth: false })
.then((res) => res.json())
expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation.id)
})
it('should populate strict access when authorized', async () => {
const doc = await restClient
.GET(`/${slug}/${post.id}`, {
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())
const doc = await restClient.GET(`/${slug}/${post.id}`).then((res) => res.json())
expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation)
})
@@ -270,22 +265,14 @@ describe('Relationships', () => {
describe('Custom ID', () => {
it('should query a custom id relation', async () => {
const { customIdRelation } = await restClient
.GET(`/${slug}/${post.id}`, {
headers: {
Authorization: `JWT ${token}`,
},
})
.GET(`/${slug}/${post.id}`)
.then((res) => res.json())
expect(customIdRelation).toMatchObject({ id: generatedCustomId })
})
it('should query a custom id number relation', async () => {
const { customIdNumberRelation } = await restClient
.GET(`/${slug}/${post.id}`, {
headers: {
Authorization: `JWT ${token}`,
},
})
.GET(`/${slug}/${post.id}`)
.then((res) => res.json())
expect(customIdNumberRelation).toMatchObject({ id: generatedCustomIdNumber })
})
@@ -312,9 +299,6 @@ describe('Relationships', () => {
it('should allow update removing a relationship', async () => {
const response = await restClient.PATCH(`/${slug}/${post.id}`, {
headers: {
Authorization: `JWT ${token}`,
},
body: JSON.stringify({
customIdRelation: null,
}),
@@ -647,9 +631,6 @@ describe('Relationships', () => {
],
},
},
headers: {
Authorization: `JWT ${token}`,
},
})
.then((res) => res.json())

View File

@@ -9,21 +9,10 @@ import Uploads2 from './collections/Upload2'
import AdminThumbnailCol from './collections/admin-thumbnail'
import { audioSlug, enlargeSlug, mediaSlug, reduceSlug, relationSlug } from './shared'
const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js')
export default buildConfigWithDefaults({
serverURL: undefined,
admin: {
webpack: (config) => ({
...config,
resolve: {
...config.resolve,
alias: {
...config?.resolve?.alias,
fs: mockModulePath,
},
},
}),
paths: {
configDir: __dirname,
},
collections: [
{

View File

@@ -1,43 +1,94 @@
import FormData from 'form-data'
import { File as FileBuffer } from 'buffer'
import NodeFormData from 'form-data'
import fs from 'fs'
import path from 'path'
import { promisify } from 'util'
import type { Payload } from '../../packages/payload/src'
import type { Enlarge, Media } from './payload-types'
import payload from '../../packages/payload/src'
import { getPayload } from '../../packages/payload/src'
import getFileByPath from '../../packages/payload/src/uploads/getFileByPath'
import { initPayloadTest } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
import { NextRESTClient } from '../helpers/NextRESTClient'
import { startMemoryDB } from '../startMemoryDB'
import configPromise from './config'
import { enlargeSlug, mediaSlug, reduceSlug, relationSlug } from './shared'
import { enlargeSlug, mediaSlug, reduceSlug, relationSlug, usersSlug } from './shared'
const getMimeType = (
filePath: string,
): {
filename: string
type: string
} => {
const ext = path.extname(filePath).slice(1)
let type: string
switch (ext) {
case 'png':
type = 'image/png'
break
case 'jpg':
type = 'image/jpeg'
break
case 'jpeg':
type = 'image/jpeg'
break
case 'svg':
type = 'image/svg+xml'
break
default:
type = 'image/png'
}
return {
filename: path.basename(filePath),
type,
}
}
const bufferToFileBlob = async (filePath: string): Promise<File> =>
new Promise((resolve, reject) => {
fs.readFile(filePath, (err, data) => {
if (err) {
console.error(`Error reading file at ${filePath}:`, err)
reject(err)
return
}
const { filename, type } = getMimeType(filePath)
// Convert type FileBuffer > unknown > File
// The File type expects webkitRelativePath, we don't have that
resolve(new FileBuffer([data], filename, { type: type }) as unknown as File)
})
})
const stat = promisify(fs.stat)
require('isomorphic-fetch')
let restClient: NextRESTClient
let payload: Payload
describe('Collections - Uploads', () => {
let client: RESTClient
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
const config = await configPromise
client = new RESTClient(config, { serverURL, defaultSlug: mediaSlug })
await client.login()
const config = await startMemoryDB(configPromise)
payload = await getPayload({ config })
restClient = new NextRESTClient(payload.config)
await restClient.login({ slug: usersSlug })
})
describe('REST', () => {
describe('create', () => {
it('creates from form data given a png', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.png')))
const filePath = path.join(__dirname, './image.png')
const { status, doc } = await client.create({
formData.append('file', await bufferToFileBlob(filePath))
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
const { sizes } = doc
const expectedPath = path.join(__dirname, './media')
@@ -64,14 +115,16 @@ describe('Collections - Uploads', () => {
it('creates from form data given an svg', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.svg')))
const filePath = path.join(__dirname, './image.svg')
formData.append('file', await bufferToFileBlob(filePath))
const { status, doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(true)
@@ -86,14 +139,16 @@ describe('Collections - Uploads', () => {
it('should have valid image url', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.png')))
const fileBlob = await bufferToFileBlob(path.join(__dirname, './image.svg'))
formData.append('file', fileBlob)
const { status, doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
const expectedPath = path.join(__dirname, './media')
expect(await fileExists(path.join(expectedPath, doc.filename))).toBe(true)
@@ -102,14 +157,16 @@ describe('Collections - Uploads', () => {
it('creates images that do not require all sizes', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')))
const fileBlob = await bufferToFileBlob(path.join(__dirname, './small.png'))
formData.append('file', fileBlob)
const { status, doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
const expectedPath = path.join(__dirname, './media')
@@ -125,14 +182,16 @@ describe('Collections - Uploads', () => {
it('creates images from a different format', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.jpg')))
const fileBlob = await bufferToFileBlob(path.join(__dirname, './image.jpg'))
formData.append('file', fileBlob)
const { status, doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
const expectedPath = path.join(__dirname, './media')
@@ -151,16 +210,17 @@ describe('Collections - Uploads', () => {
it('creates media without storing a file', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './unstored.png')))
const fileBlob = await bufferToFileBlob(path.join(__dirname, './unstored.png'))
formData.append('file', fileBlob)
// unstored media
const { status, doc } = await client.create({
slug: 'unstored-media',
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
const { doc } = await response.json()
expect(status).toBe(201)
expect(response.status).toBe(201)
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(false)
@@ -242,7 +302,7 @@ describe('Collections - Uploads', () => {
})
it('should not reduce images if resize options `withoutReduction` is set to true', async () => {
const formData = new FormData()
const formData = new NodeFormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')))
const small = await getFileByPath(path.resolve(__dirname, './small.png'))
@@ -296,14 +356,14 @@ describe('Collections - Uploads', () => {
})) as unknown as Media
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')))
formData.append('file', await bufferToFileBlob(path.join(__dirname, './small.png')))
const { status } = await client.update({
id: mediaDoc.id,
data: formData,
const response = await restClient.PATCH(`/${mediaSlug}/${mediaDoc.id}`, {
body: formData,
file: true,
})
expect(status).toBe(200)
expect(response.status).toBe(200)
const expectedPath = path.join(__dirname, './media')
@@ -325,17 +385,21 @@ describe('Collections - Uploads', () => {
})) as unknown as Media
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')))
formData.append('file', await bufferToFileBlob(path.join(__dirname, './small.png')))
const { status } = await client.updateMany({
// id: mediaDoc.id,
where: {
id: { equals: mediaDoc.id },
const response = await restClient.PATCH(`/${mediaSlug}`, {
body: formData,
file: true,
query: {
where: {
id: {
equals: mediaDoc.id,
},
},
},
data: formData,
})
expect(status).toBe(200)
expect(response.status).toBe(200)
const expectedPath = path.join(__dirname, './media')
@@ -522,37 +586,43 @@ describe('Collections - Uploads', () => {
it('delete', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.png')))
formData.append('file', await bufferToFileBlob(path.join(__dirname, './image.png')))
const { doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
expect(response.status).toBe(200)
const { status } = await client.delete(doc.id, {
id: doc.id,
})
expect(status).toBe(200)
const { doc } = await response.json()
const response2 = await restClient.DELETE(`/${mediaSlug}/${doc.id}`)
expect(response2.status).toBe(200)
expect(await fileExists(path.join(__dirname, doc.filename))).toBe(false)
})
it('delete - update many', async () => {
const formData = new FormData()
formData.append('file', fs.createReadStream(path.join(__dirname, './image.png')))
formData.append('file', await bufferToFileBlob(path.join(__dirname, './image.png')))
const { doc } = await client.create({
const response = await restClient.POST(`/${mediaSlug}`, {
body: formData,
file: true,
data: formData,
})
expect(response.status).toBe(200)
const { doc } = await response.json()
const { errors } = await client.deleteMany({
slug: mediaSlug,
where: {
id: { equals: doc.id },
},
})
const { errors } = await restClient
.DELETE(`/${mediaSlug}`, {
query: {
where: {
id: {
equals: doc.id,
},
},
},
})
.then((res) => res.json())
expect(errors).toHaveLength(0)

View File

@@ -1,3 +1,5 @@
export const usersSlug = 'users'
export const mediaSlug = 'media'
export const relationSlug = 'relation'

View File

@@ -41,7 +41,7 @@
"@payloadcms/translations/api": ["./packages/translations/src/all"],
"@payloadcms/next/*": ["./packages/next/src/*"],
"@payloadcms/graphql": ["./packages/graphql/src"],
"payload-config": ["./test/auth/config.ts"]
"payload-config": ["./test/uploads/config.ts"]
}
},
"exclude": ["dist", "build", "temp", "node_modules"],