Files
payload/test/collections-rest/int.spec.ts
Sasha 749962a1db fix: upload and auth endpoints are mounted for all collections (#11231)
This PR ensures, that collections that don't have `auth: true` don't
mount authentication related endpoints like `/me`, the same for uploads.
Additionally, moves upload-related endpoints to `uploads/endpoints/*`.
2025-02-17 21:11:40 +02:00

1826 lines
56 KiB
TypeScript

import type { Payload, SanitizedCollectionConfig } from 'payload'
import { randomBytes, randomUUID } from 'crypto'
import path from 'path'
import { APIError, NotFound } from 'payload'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import type { Relation } from './config.js'
import type { Post } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import {
customIdNumberSlug,
customIdSlug,
endpointsSlug,
errorOnHookSlug,
methods,
pointSlug,
postsSlug,
relationSlug,
} from './config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
let restClient: NextRESTClient
let payload: Payload
describe('collections-rest', () => {
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
beforeEach(async () => {
await clearDocs()
})
describe('CRUD', () => {
it('should create', async () => {
const data = {
title: 'title',
}
const doc = await createPost(data)
expect(doc).toMatchObject(data)
})
it('should find', async () => {
const post1 = await createPost()
const post2 = await createPost()
const response = await restClient.GET(`/${postsSlug}`)
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(2)
const expectedDocs = [post1, post2]
expect(result.docs).toHaveLength(expectedDocs.length)
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
it('should count', async () => {
await createPost()
await createPost()
const response = await restClient.GET(`/${postsSlug}/count`)
const result = await response.json()
expect(response.status).toEqual(200)
expect(result).toEqual({ totalDocs: 2 })
})
it('should find where id', async () => {
const post1 = await createPost()
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { id: { equals: post1.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs[0].id).toEqual(post1.id)
})
it('should find with pagination false', async () => {
const post1 = await createPost()
const post2 = await createPost()
const { docs, totalDocs } = await payload.find({
collection: postsSlug,
overrideAccess: false,
pagination: false,
})
const expectedDocs = [post1, post2]
expect(docs).toHaveLength(expectedDocs.length)
expect(docs).toEqual(expect.arrayContaining(expectedDocs))
expect(totalDocs).toEqual(2)
})
it('should update existing', async () => {
const { id, description } = await createPost({ description: 'desc' })
const updatedTitle = 'updated-title'
const response = await restClient.PATCH(`/${postsSlug}/${id}`, {
body: JSON.stringify({ title: updatedTitle }),
})
const { doc } = await response.json()
expect(response.status).toEqual(200)
expect(doc.title).toEqual(updatedTitle)
expect(doc.description).toEqual(description) // Check was not modified
})
describe('Bulk operations', () => {
it('should bulk update', async () => {
for (let i = 0; i < 11; i++) {
await createPost({ description: `desc ${i}` })
}
const description = 'updated'
const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { where: { title: { equals: 'title' } } },
})
const { docs, errors } = await response.json()
expect(errors).toHaveLength(0)
expect(response.status).toEqual(200)
expect(docs[0].title).toEqual('title') // Check was not modified
expect(docs[0].description).toEqual(description)
expect(docs.pop().description).toEqual(description)
})
it('should bulk update with limit', async () => {
const ids = []
for (let i = 0; i < 3; i++) {
const post = await createPost({ description: `to-update` })
ids.push(post.id)
}
const description = 'updated-description'
const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { limit: 2, where: { id: { in: ids } } },
})
const { docs, errors } = await response.json()
expect(errors).toHaveLength(0)
expect(response.status).toEqual(200)
expect(docs).toHaveLength(2)
expect(docs[0].description).toEqual(description)
expect(docs.pop().description).toEqual(description)
const { docs: resDocs } = await payload.find({
limit: 10,
collection: postsSlug,
where: { id: { in: ids } },
})
expect(resDocs.at(-1).description).toEqual('to-update')
})
it('should not bulk update with a bad query', async () => {
for (let i = 0; i < 2; i++) {
await createPost({ description: `desc ${i}` })
}
const description = 'updated'
const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { where: { missing: { equals: 'title' } } },
})
const { docs: noDocs, errors } = await response.json()
expect(response.status).toEqual(400)
expect(noDocs).toBeUndefined()
expect(errors).toHaveLength(1)
const { docs } = await payload.find({
collection: postsSlug,
})
expect(docs[0].description).not.toEqual(description)
expect(docs.pop().description).not.toEqual(description)
})
it('should not bulk update with a bad relationship query', async () => {
for (let i = 0; i < 2; i++) {
await createPost({ description: `desc ${i}` })
}
const description = 'updated'
const relationFieldResponse = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { where: { 'relationField.missing': { equals: 'title' } } },
})
expect(relationFieldResponse.status).toEqual(400)
const relationMultiRelationToResponse = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { where: { 'relationMultiRelationTo.missing': { equals: 'title' } } },
})
expect(relationMultiRelationToResponse.status).toEqual(400)
const { docs } = await payload.find({
collection: postsSlug,
})
expect(docs[0].description).not.toEqual(description)
expect(docs.pop().description).not.toEqual(description)
})
it('should not bulk update with a read restricted field query', async () => {
const { id } = await payload.create({
collection: postsSlug,
data: {
restrictedField: 'restricted',
},
})
const description = 'description'
const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({
description,
}),
query: { where: { restrictedField: { equals: 'restricted' } } },
})
const result = await response.json()
const doc = await payload.findByID({
id,
collection: postsSlug,
})
expect(response.status).toEqual(400)
expect(result.errors).toHaveLength(1)
expect(result.errors[0].message).toEqual(
'The following path cannot be queried: restrictedField',
)
expect(doc.description).toBeFalsy()
})
it('should return formatted errors for bulk updates', async () => {
const text = 'bulk-update-test-errors'
const errorDoc = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: true,
text,
},
})
const successDoc = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: false,
text,
},
})
const update = 'update'
const response = await restClient.PATCH(`/${errorOnHookSlug}`, {
body: JSON.stringify({
text: update,
}),
query: { where: { text: { equals: text } } },
})
const result = await response.json()
expect(response.status).toEqual(400)
expect(result.docs).toHaveLength(1)
expect(result.docs[0].id).toEqual(successDoc.id)
expect(result.errors).toHaveLength(1)
expect(result.errors[0].message).toBeDefined()
expect(result.errors[0].id).toEqual(errorDoc.id)
expect(result.docs[0].text).toEqual(update)
})
it('should bulk delete', async () => {
const count = 11
for (let i = 0; i < count; i++) {
await createPost({ description: `desc ${i}` })
}
const response = await restClient.DELETE(`/${postsSlug}`, {
query: { where: { title: { equals: 'title' } } },
})
const { docs } = await response.json()
expect(response.status).toEqual(200)
expect(docs[0].title).toEqual('title') // Check was not modified
expect(docs).toHaveLength(count)
})
it('should return formatted errors for bulk deletes', async () => {
await payload.create({
collection: errorOnHookSlug,
data: {
errorAfterDelete: true,
text: 'test',
},
})
await payload.create({
collection: errorOnHookSlug,
data: {
errorAfterDelete: false,
text: 'test',
},
})
const response = await restClient.DELETE(`/${errorOnHookSlug}`, {
query: { where: { text: { equals: 'test' } } },
})
const result = await response.json()
expect(response.status).toEqual(400)
expect(result.docs).toHaveLength(1)
expect(result.errors).toHaveLength(1)
expect(result.errors[0].message).toBeDefined()
expect(result.errors[0].id).toBeDefined()
})
})
describe('Custom ID', () => {
describe('string', () => {
it('should create', async () => {
const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`
const customIdName = 'custom-id-name'
const { doc } = await restClient
.POST(`/${customIdSlug}`, {
body: JSON.stringify({ id: customId, name: customIdName }),
})
.then((res) => res.json())
expect(doc.id).toEqual(customId)
expect(doc.name).toEqual(customIdName)
})
it('should find', async () => {
const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`
const { doc } = await restClient
.POST(`/${customIdSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-name' }),
})
.then((res) => res.json())
const { id } = await restClient
.GET(`/${customIdSlug}/${customId}`)
.then((res) => res.json())
expect(id).toEqual(doc.id)
})
it('should query - equals', async () => {
const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`
const { doc } = await restClient
.POST(`/${customIdSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-name' }),
})
.then((res) => res.json())
const { docs } = await restClient
.GET(`/${customIdSlug}`, {
query: {
where: { id: { equals: customId } },
},
})
.then((res) => res.json())
expect(docs.map(({ id }) => id)).toContain(doc.id)
})
it('should query - like', async () => {
const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`
const { doc } = await restClient
.POST(`/${customIdSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-name' }),
})
.then((res) => res.json())
const { docs } = await restClient
.GET(`/${customIdSlug}`, {
query: {
where: { id: { like: 'custom' } },
},
})
.then((res) => res.json())
expect(docs.map(({ id }) => id)).toContain(doc.id)
})
it('should update', async () => {
const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`
const { doc } = await restClient
.POST(`/${customIdSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-name' }),
})
.then((res) => res.json())
const { doc: updatedDoc } = await restClient
.PATCH(`/${customIdSlug}/${doc.id}`, {
body: JSON.stringify({ name: 'updated' }),
})
.then((res) => res.json())
expect(updatedDoc.name).toEqual('updated')
})
})
describe('number', () => {
it('should create', async () => {
const customId = Math.floor(Math.random() * 1_000_000) + 1
const { doc } = await restClient
.POST(`/${customIdNumberSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }),
})
.then((res) => res.json())
expect(doc.id).toEqual(customId)
})
it('should find', async () => {
const customId = Math.floor(Math.random() * 1_000_000) + 1
const { doc } = await restClient
.POST(`/${customIdNumberSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }),
})
.then((res) => res.json())
const { id } = await restClient
.GET(`/${customIdNumberSlug}/${customId}`)
.then((res) => res.json())
expect(id).toEqual(doc.id)
})
it('should update', async () => {
const customId = Math.floor(Math.random() * 1_000_000) + 1
const { doc } = await restClient
.POST(`/${customIdNumberSlug}`, {
body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }),
})
.then((res) => res.json())
const { doc: updatedDoc } = await restClient
.PATCH(`/${customIdNumberSlug}/${doc.id}`, {
body: JSON.stringify({ name: 'updated' }),
})
.then((res) => res.json())
expect(updatedDoc.name).toEqual('updated')
})
it('should allow querying by in', async () => {
const id = 98234698237
await restClient.POST(`/${customIdNumberSlug}`, {
body: JSON.stringify({ id, name: 'query using in operator' }),
})
const { docs } = await restClient
.GET(`/${customIdNumberSlug}`, {
query: {
where: { id: { in: `${id}, ${2349856723948764}` } },
},
})
.then((res) => res.json())
expect(docs).toHaveLength(1)
})
})
})
it('should delete', async () => {
const { id } = await createPost()
const response = await restClient.DELETE(`/${postsSlug}/${id}`)
const { doc } = await response.json()
expect(response.status).toEqual(200)
expect(doc.id).toEqual(id)
})
it('should include metadata', async () => {
await createPosts(11)
const result = await restClient.GET(`/${postsSlug}`).then((res) => res.json())
expect(result.totalDocs).toBeGreaterThan(0)
expect(result.limit).toBe(10)
expect(result.page).toBe(1)
expect(result.pagingCounter).toBe(1)
expect(result.hasPrevPage).toBe(false)
expect(result.hasNextPage).toBe(true)
expect(result.prevPage).toBeNull()
expect(result.nextPage).toBe(2)
})
})
describe('Querying', () => {
it.todo('should allow querying by a field within a group')
describe('Relationships', () => {
let post: Post
let relation: Relation
let relation2: Relation
const nameToQuery = 'name'
const nameToQuery2 = 'name2'
beforeEach(async () => {
;({ doc: relation } = await restClient
.POST(`/${relationSlug}`, {
body: JSON.stringify({ name: nameToQuery }),
})
.then((res) => res.json()))
;({ doc: relation2 } = await restClient
.POST(`/${relationSlug}`, {
body: JSON.stringify({ name: nameToQuery2 }),
})
.then((res) => res.json()))
post = await createPost({
relationField: relation.id,
})
await createPost() // Extra post to allow asserting totalDoc count
})
describe('regular relationship', () => {
it('query by property value', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post])
expect(result.totalDocs).toEqual(1)
})
it('should count query by property value', async () => {
const response = await restClient.GET(`/${postsSlug}/count`, {
query: {
where: { relationField: { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result).toEqual({ totalDocs: 1 })
})
it('query by id', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post])
expect(result.totalDocs).toEqual(1)
})
it('should query LIKE by ID', async () => {
const post = await payload.create({
collection: postsSlug,
data: {
title: 'find me buddy',
},
})
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
id: {
like: post.id,
},
},
},
})
const result = await response.json()
expect(response.status).toStrictEqual(200)
expect(result.totalDocs).toStrictEqual(1)
})
})
it('should query nested relationship - hasMany', async () => {
const post1 = await createPost({
relationHasManyField: [relation.id, relation2.id],
})
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { 'relationHasManyField.name': { equals: relation.name } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
// Query second relationship
const response2 = await restClient.GET(`/${postsSlug}`, {
query: {
where: { 'relationHasManyField.name': { equals: relation2.name } },
},
})
const result2 = await response2.json()
expect(response2.status).toEqual(200)
expect(result2.docs).toEqual([post1])
expect(result2.totalDocs).toEqual(1)
})
describe('relationTo multi', () => {
it('nested by id', async () => {
const post1 = await createPost({
relationMultiRelationTo: { relationTo: relationSlug, value: relation.id },
})
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { 'relationMultiRelationTo.value': { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
})
})
it('should query relationships by not_equals', async () => {
const ogPost = await createPost({
relationMultiRelationTo: { relationTo: relationSlug, value: relation.id },
})
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
and: [
{
'relationMultiRelationTo.value': { not_equals: relation.id },
},
],
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
const foundExcludedDoc = result.docs.some((doc) => ogPost.id === doc.id)
expect(foundExcludedDoc).toBe(false)
})
describe('relationTo multi hasMany', () => {
it('nested by id', async () => {
const post1 = await createPost({
relationMultiRelationToHasMany: [
{ relationTo: relationSlug, value: relation.id },
{ relationTo: relationSlug, value: relation2.id },
],
})
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
// Query second relation
const response2 = await restClient.GET(`/${postsSlug}`, {
query: {
where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } },
},
})
const result2 = await response2.json()
expect(response2.status).toEqual(200)
expect(result2.docs).toEqual([post1])
expect(result2.totalDocs).toEqual(1)
})
it.todo('nested by property value')
})
})
describe('Edge cases', () => {
it('should query a localized field without localization configured', async () => {
const test = 'test'
await createPost({ fakeLocalization: test })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { fakeLocalization: { equals: test } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not error when attempting to sort on a field that does not exist', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
sort: 'fake',
},
})
expect(response.status).toStrictEqual(200)
})
})
describe('Operators', () => {
it('equals', async () => {
const valueToQuery = 'valueToQuery'
const post1 = await createPost({ title: valueToQuery })
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { title: { equals: valueToQuery } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([post1])
})
it('not_equals', async () => {
const post1 = await createPost({ title: 'not-equals' })
const post2 = await createPost()
const post3 = await createPost({ title: undefined })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { title: { not_equals: post1.title } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(2)
expect(result.docs).toEqual([post3, post2])
})
it('in', async () => {
const post1 = await createPost({ title: 'my-title' })
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { title: { in: [post1.title] } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
})
it('not_in', async () => {
const post1 = await createPost({ title: 'not-me' })
const post2 = await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { title: { not_in: [post1.title] } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post2])
expect(result.totalDocs).toEqual(1)
})
it('not_in (relationships)', async () => {
const relationship = await payload.create({
collection: relationSlug,
data: {},
})
await createPost({ relationField: relationship.id, title: 'not-me' })
// await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' })
const post2 = await createPost({ title: 'me' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { not_in: [relationship.id] } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post2])
expect(result.totalDocs).toEqual(1)
// do not want to error for empty arrays
const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { not_in: [] } },
},
})
expect(emptyNotInResponse.status).toEqual(200)
})
it('in (relationships)', async () => {
const relationship = await payload.create({
collection: relationSlug,
data: {},
})
const post1 = await createPost({ relationField: relationship.id, title: 'me' })
// await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' })
await createPost({ title: 'not-me' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { in: [relationship.id] } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
// do not want to error for empty arrays
const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, {
query: {
where: { relationField: { in: [] } },
},
})
expect(emptyNotInResponse.status).toEqual(200)
})
it('like', async () => {
const post1 = await createPost({ title: 'prefix-value' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: { title: { like: 'prefix' } },
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
})
describe('like - special characters', () => {
const specialCharacters = '~!@#$%^&*()_+-+[]{}|;:"<>,.?/})'
it.each(specialCharacters.split(''))(
'like - special characters - %s',
async (character) => {
const post1 = await createPost({
title: specialCharacters,
})
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
title: {
like: character,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
},
)
})
it('like - cyrillic characters', async () => {
const post1 = await createPost({ title: 'Тест' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
title: {
like: 'Тест',
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
})
it('like - cyrillic characters in multiple words', async () => {
const post1 = await createPost({ title: 'привет, это тест полезной нагрузки' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
title: {
like: 'привет нагрузки',
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post1])
expect(result.totalDocs).toEqual(1)
})
it('like - partial word match', async () => {
const post = await createPost({ title: 'separate words should partially match' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
title: {
like: 'words partial',
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toEqual([post])
expect(result.totalDocs).toEqual(1)
})
it('like - id should not crash', async () => {
const post = await createPost({ title: 'post' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
id: {
like: 'words partial',
},
},
},
})
expect(response.status).toEqual(200)
})
it('exists - true', async () => {
const postWithDesc = await createPost({ description: 'exists' })
await createPost({ description: undefined })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
description: {
exists: true,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([postWithDesc])
})
it('exists - false', async () => {
const postWithoutDesc = await createPost({ description: undefined })
await createPost({ description: 'exists' })
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
description: {
exists: false,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([postWithoutDesc])
})
describe('numbers', () => {
let post1: Post
let post2: Post
beforeEach(async () => {
post1 = await createPost({ number: 1 })
post2 = await createPost({ number: 2 })
})
it('greater_than', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
number: {
greater_than: 1,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([post2])
})
it('greater_than_equal', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
number: {
greater_than_equal: 1,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(2)
const expectedDocs = [post1, post2]
expect(result.docs).toHaveLength(expectedDocs.length)
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
it('less_than', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
number: {
less_than: 2,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([post1])
})
it('less_than_equal', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
number: {
less_than_equal: 2,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(2)
const expectedDocs = [post1, post2]
expect(result.docs).toHaveLength(expectedDocs.length)
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
})
describe('near', () => {
const point = [10, 20]
const [lat, lng] = point
it('should return a document near a point', async () => {
if (payload.db.name === 'sqlite') {
return
}
const near = `${lat + 0.01}, ${lng + 0.01}, 10000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
const responseCount = await restClient.GET(`/${pointSlug}/count`, {
query: {
where: {
point: {
near,
},
},
},
})
const resultCount = await responseCount.json()
expect(responseCount.status).toEqual(200)
expect(resultCount.totalDocs).toBe(1)
})
it('should omit maxDistance and return a document from minDistance', async () => {
if (payload.db.name === 'sqlite') {
return
}
const near = `${lat + 0.01}, ${lng + 0.01}, null, 1500`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should omit maxDistance and not return a document because exceeds minDistance', async () => {
if (payload.db.name === 'sqlite') {
return
}
const near = `${lat + 0.01}, ${lng + 0.01}, null, 1700`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
it('should not return a point far away', async () => {
if (payload.db.name === 'sqlite') {
return
}
const near = `${lng + 1}, ${lat + 1}, 5000`
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
near,
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
it('should sort find results by nearest distance', async () => {
if (payload.db.name === 'sqlite') {
return
}
// creating twice as many records as we are querying to get a random sample
const promises = []
for (let i = 0; i < 11; i++) {
// setTimeout used to randomize the creation timestamp
setTimeout(() => {
promises.push(
payload.create({
collection: pointSlug,
data: {
// only randomize longitude to make distance comparison easy
point: [Math.random(), 0],
},
}),
)
}, Math.random())
}
await Promise.all(promises)
const { docs } = await restClient
.GET(`/${pointSlug}`, {
query: {
limit: 5,
sort: 'point',
where: {
point: {
// querying large enough range to include all docs
near: '0, 0, 100000, 0',
},
},
},
})
.then((res) => res.json())
let previous = 0
docs.forEach(({ point: coordinates }) => {
// the next document point should always be greater than the one before
expect(previous).toBeLessThanOrEqual(coordinates[0])
;[previous] = coordinates
})
})
})
describe('within', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point inside the polygon', async () => {
if (payload.db.name === 'sqlite') {
return
}
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not return a document with the point outside a smaller polygon', async () => {
if (payload.db.name === 'sqlite') {
return
}
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
within: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
})
describe('intersects', () => {
type Point = [number, number]
const polygon: Point[] = [
[9.0, 19.0], // bottom-left
[9.0, 21.0], // top-left
[11.0, 21.0], // top-right
[11.0, 19.0], // bottom-right
[9.0, 19.0], // back to starting point to close the polygon
]
it('should return a document with the point intersecting the polygon', async () => {
if (payload.db.name === 'sqlite') {
return
}
// There should be 1 total points document populated by default with the point [10, 20]
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon],
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(1)
})
it('should not return a document with the point not intersecting a smaller polygon', async () => {
if (payload.db.name === 'sqlite') {
return
}
const response = await restClient.GET(`/${pointSlug}`, {
query: {
where: {
point: {
intersects: {
type: 'Polygon',
coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size
},
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(0)
})
})
it('or', async () => {
const post1 = await createPost({ title: 'post1' })
const post2 = await createPost({ title: 'post2' })
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
or: [
{
title: {
equals: post1.title,
},
},
{
title: {
equals: post2.title,
},
},
],
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
const expectedDocs = [post1, post2]
expect(result.totalDocs).toEqual(expectedDocs.length)
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
it('or - 1 result', async () => {
const post1 = await createPost({ title: 'post1' })
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
or: [
{
title: {
equals: post1.title,
},
},
{
title: {
equals: 'non-existent',
},
},
],
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
const expectedDocs = [post1]
expect(result.totalDocs).toEqual(expectedDocs.length)
expect(result.docs).toEqual(expect.arrayContaining(expectedDocs))
})
it('and', async () => {
const description = 'description'
const post1 = await createPost({ description, title: 'post1' })
await createPost({ description, title: 'post2' }) // Diff title, same desc
await createPost()
const response = await restClient.GET(`/${postsSlug}`, {
query: {
where: {
and: [
{
title: {
equals: post1.title,
},
},
{
description: {
equals: description,
},
},
],
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([post1])
})
describe('pagination', () => {
let relatedDoc
beforeEach(async () => {
relatedDoc = await payload.create({
collection: relationSlug,
data: {
name: 'test',
},
})
for (let i = 0; i < 10; i++) {
await createPost({
number: i,
relationField: relatedDoc.id,
title: 'paginate-test',
})
}
})
it('should paginate with where query', async () => {
const query = {
limit: 4,
where: {
title: {
equals: 'paginate-test',
},
},
}
let response = await restClient.GET(`/${postsSlug}`, { query })
const page1 = await response.json()
response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } })
const page2 = await response.json()
response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } })
const page3 = await response.json()
expect(page1.hasNextPage).toStrictEqual(true)
expect(page1.hasPrevPage).toStrictEqual(false)
expect(page1.limit).toStrictEqual(4)
expect(page1.nextPage).toStrictEqual(2)
expect(page1.page).toStrictEqual(1)
expect(page1.pagingCounter).toStrictEqual(1)
expect(page1.prevPage).toStrictEqual(null)
expect(page1.totalDocs).toStrictEqual(10)
expect(page1.totalPages).toStrictEqual(3)
expect(page2.hasNextPage).toStrictEqual(true)
expect(page2.hasPrevPage).toStrictEqual(true)
expect(page2.limit).toStrictEqual(4)
expect(page2.nextPage).toStrictEqual(3)
expect(page2.page).toStrictEqual(2)
expect(page2.pagingCounter).toStrictEqual(5)
expect(page2.prevPage).toStrictEqual(1)
expect(page2.totalDocs).toStrictEqual(10)
expect(page2.totalPages).toStrictEqual(3)
expect(page3.hasNextPage).toStrictEqual(false)
expect(page3.hasPrevPage).toStrictEqual(true)
expect(page3.limit).toStrictEqual(4)
expect(page3.nextPage).toStrictEqual(null)
expect(page3.page).toStrictEqual(3)
expect(page3.pagingCounter).toStrictEqual(9)
expect(page3.prevPage).toStrictEqual(2)
expect(page3.totalDocs).toStrictEqual(10)
expect(page3.totalPages).toStrictEqual(3)
})
it('should paginate with where query on relationships', async () => {
const query = {
limit: 4,
where: {
relationField: {
equals: relatedDoc.id,
},
},
}
let response = await restClient.GET(`/${postsSlug}`, { query })
const page1 = await response.json()
response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } })
const page2 = await response.json()
response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } })
const page3 = await response.json()
expect(page1.hasNextPage).toStrictEqual(true)
expect(page1.hasPrevPage).toStrictEqual(false)
expect(page1.limit).toStrictEqual(4)
expect(page1.nextPage).toStrictEqual(2)
expect(page1.page).toStrictEqual(1)
expect(page1.pagingCounter).toStrictEqual(1)
expect(page1.prevPage).toStrictEqual(null)
expect(page1.totalDocs).toStrictEqual(10)
expect(page1.totalPages).toStrictEqual(3)
expect(page2.hasNextPage).toStrictEqual(true)
expect(page2.hasPrevPage).toStrictEqual(true)
expect(page2.limit).toStrictEqual(4)
expect(page2.nextPage).toStrictEqual(3)
expect(page2.page).toStrictEqual(2)
expect(page2.pagingCounter).toStrictEqual(5)
expect(page2.prevPage).toStrictEqual(1)
expect(page2.totalDocs).toStrictEqual(10)
expect(page2.totalPages).toStrictEqual(3)
expect(page3.hasNextPage).toStrictEqual(false)
expect(page3.hasPrevPage).toStrictEqual(true)
expect(page3.limit).toStrictEqual(4)
expect(page3.nextPage).toStrictEqual(null)
expect(page3.page).toStrictEqual(3)
expect(page3.pagingCounter).toStrictEqual(9)
expect(page3.prevPage).toStrictEqual(2)
expect(page3.totalDocs).toStrictEqual(10)
expect(page3.totalPages).toStrictEqual(3)
})
describe('limit', () => {
beforeEach(async () => {
for (let i = 0; i < 50; i++) {
await createPost({ number: i, title: 'limit-test' })
}
})
it('should query a limited set of docs', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
limit: 15,
where: {
title: {
equals: 'limit-test',
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(15)
})
it('should query all docs when limit=0', async () => {
const response = await restClient.GET(`/${postsSlug}`, {
query: {
limit: 0,
where: {
title: {
equals: 'limit-test',
},
},
},
})
const result = await response.json()
expect(response.status).toEqual(200)
expect(result.docs).toHaveLength(50)
expect(result.totalPages).toEqual(1)
})
})
})
it('can query deeply nested fields within rows, tabs, collapsibles', async () => {
const withDeeplyNestedField = await createPost({
D1: { D2: { D3: { D4: 'nested message' } } },
})
const result = await restClient
.GET(`/${postsSlug}`, {
query: {
where: {
'D1.D2.D3.D4': {
equals: 'nested message',
},
},
},
})
.then((res) => res.json())
expect(result.totalDocs).toEqual(1)
expect(result.docs).toEqual([withDeeplyNestedField])
})
})
})
describe('Error Handler', () => {
it('should return the minimum allowed information about internal errors', async () => {
const response = await restClient.GET('/internal-error-here')
const result = await response.json()
expect(response.status).toBe(500)
expect(Array.isArray(result.errors)).toEqual(true)
expect(result.errors[0].message).toStrictEqual('Something went wrong.')
})
it('should execute afterError hook on root level and modify result/status', async () => {
let err: unknown
let errResult: any
payload.config.hooks.afterError = [
({ error, result }) => {
err = error
errResult = result
return { status: 400, response: { modified: true } }
},
]
const response = await restClient.GET(`/api-error-here`)
expect(response.status).toBe(400)
expect(err).toBeInstanceOf(APIError)
expect(errResult).toStrictEqual({
errors: [
{
message: 'Something went wrong.',
},
],
})
const result = await response.json()
expect(result.modified).toBe(true)
payload.config.hooks.afterError = []
})
it('should execute afterError hook on collection level and modify result', async () => {
let err: unknown
let errResult: any
let collection: SanitizedCollectionConfig
payload.collections.posts.config.hooks.afterError = [
({ error, result, collection: incomingCollection }) => {
err = error
errResult = result
collection = incomingCollection
return { response: { modified: true } }
},
]
const post = await createPost({})
const response = await restClient.GET(
`/${postsSlug}/${typeof post.id === 'number' ? 1000 : randomUUID()}`,
)
expect(response.status).toBe(404)
expect(collection.slug).toBe(postsSlug)
expect(err).toBeInstanceOf(NotFound)
expect(errResult).toStrictEqual({
errors: [
{
message: 'Not Found',
},
],
})
const result = await response.json()
expect(result.modified).toBe(true)
payload.collections.posts.config.hooks.afterError = []
})
})
describe('Local', () => {
it('findByID should throw NotFound if the doc was not found, if disableErrors: true then return null', async () => {
const post = await createPost()
const id = typeof post.id === 'string' ? randomUUID() : 999
await expect(payload.findByID({ collection: 'posts', id })).rejects.toBeInstanceOf(NotFound)
await expect(
payload.findByID({ collection: 'posts', id, disableErrors: true }),
).resolves.toBeNull()
})
})
describe('Custom endpoints', () => {
it('should execute custom root endpoints', async () => {
for (const method of methods) {
const response = await restClient[method.toUpperCase()](`/${method}-test`, {})
await expect(response.text()).resolves.toBe(`${method} response`)
}
})
it('should execute custom collection endpoints', async () => {
for (const method of methods) {
const response = await restClient[method.toUpperCase()](
`/${endpointsSlug}/${method}-test`,
{},
)
await expect(response.text()).resolves.toBe(`${method} response`)
}
})
})
it('should not mount auth endpoints for collection without auth', async () => {
const authEndpoints = [
{
method: 'post',
path: '/forgot-password',
},
{
method: 'post',
path: '/login',
},
{
method: 'post',
path: '/logout',
},
{
method: 'post',
path: '/refresh-token',
},
{
method: 'post',
path: '/first-register',
},
{
method: 'post',
path: '/reset-password',
},
{
method: 'post',
path: '/unlock',
},
]
for (const endpoint of authEndpoints) {
const result = await restClient[endpoint.method.toUpperCase()](
`/${endpointsSlug}${endpoint.path}`,
)
expect(result.status).toBe(404)
const json = await result.json()
expect(json.message.startsWith('Route not found')).toBeTruthy()
}
})
it('should not mount upload endpoints for collection without auth', async () => {
const uploadEndpoints = [
{
method: 'get',
path: '/paste-url/some-id',
},
{
method: 'get',
path: '/file/some-filename.png',
},
]
for (const endpoint of uploadEndpoints) {
const result = await restClient[endpoint.method.toUpperCase()](
`/${endpointsSlug}${endpoint.path}`,
)
expect(result.status).toBe(404)
expect((await result.json()).message.startsWith('Route not found')).toBeTruthy()
}
})
})
async function createPost(overrides?: Partial<Post>) {
const { doc } = await restClient
.POST(`/${postsSlug}`, {
body: JSON.stringify({ title: 'title', ...overrides }),
})
.then((res) => res.json())
return doc
}
async function createPosts(count: number) {
for (let i = 0; i < count; i++) {
await createPost()
}
}
async function clearDocs(): Promise<void> {
await payload.delete({
collection: postsSlug,
where: { id: { exists: true } },
})
}