Files
payloadcms/test/locked-documents/int.spec.ts
Patrik c63b7bc606 fix: disables document locking of payload-locked-documents, preferences, & migrations collections (#8744)
Fixes #8589 

### Issue: 
There were problems with updating documents in
`payload-locked-documents` collection i.e when "taking over" a document
- a `patch` request is sent to `payload-locked-documents` to update the
user (owner).

However, as a result, this `update` operation would lock that
corresponding doc in `payload-locked-documents` and therefore error on
the `patch` request.

### Fix:
Disable document locking entirely from `payload-locked-documents` &
`preferences` & `migrations` collections
2024-10-16 14:46:39 -04:00

659 lines
17 KiB
TypeScript

import type { Payload, SanitizedCollectionConfig } from 'payload'
import path from 'path'
import { Locked, NotFound } from 'payload'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import type { Menu, Page, Post } from './payload-types.js'
import { devUser } from '../credentials.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { pagesSlug } from './collections/Pages/index.js'
import { postsSlug } from './collections/Posts/index.js'
import { menuSlug } from './globals/Menu/index.js'
const lockedDocumentCollection = 'payload-locked-documents'
let payload: Payload
let token: string
let restClient: NextRESTClient
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('Locked documents', () => {
let post: Post
let page: Page
let menu: Menu
let user: any
let user2: any
let postConfig: SanitizedCollectionConfig
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
postConfig = payload.config.collections.find(({ slug }) => slug === postsSlug)
const loginResult = await payload.login({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
user = loginResult.user
token = loginResult.token
user2 = await payload.create({
collection: 'users',
data: {
email: 'test@payloadcms.com',
password: 'test',
},
})
post = await payload.create({
collection: postsSlug,
data: {
text: 'some post',
},
})
page = await payload.create({
collection: pagesSlug,
data: {
text: 'some page',
},
})
menu = await payload.updateGlobal({
slug: menuSlug,
data: {
globalText: 'global text',
},
})
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
afterEach(() => {
postConfig.lockDocuments = { duration: 300 }
})
it('should update unlocked document - collection', async () => {
const updatedPost = await payload.update({
collection: postsSlug,
data: {
text: 'updated post',
},
id: post.id,
})
expect(updatedPost.text).toEqual('updated post')
})
it('should update unlocked document - global', async () => {
const updatedGlobalMenu = await payload.updateGlobal({
slug: menuSlug,
data: {
globalText: 'updated global text',
},
})
expect(updatedGlobalMenu.globalText).toEqual('updated global text')
})
it('should delete unlocked document - collection', async () => {
const { docs } = await payload.find({
collection: postsSlug,
depth: 0,
})
expect(docs).toHaveLength(2)
await payload.delete({
collection: postsSlug,
id: post.id,
})
const { docs: deletedResults } = await payload.find({
collection: postsSlug,
depth: 0,
})
expect(deletedResults).toHaveLength(1)
})
it('should allow update of stale locked document - collection', async () => {
const newPost2 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 2',
},
})
// Set lock duration to 1 second for testing purposes
postConfig.lockDocuments = { duration: 1 }
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost2.id,
},
globalSlug: undefined,
},
})
await wait(1100)
const updateLockedDoc = await payload.update({
collection: postsSlug,
data: {
text: 'updated post 2',
},
overrideLock: false,
id: newPost2.id,
})
postConfig.lockDocuments = { duration: 300 }
// Should allow update since editedAt date is past expiration duration.
// Therefore the document is considered stale
expect(updateLockedDoc.text).toEqual('updated post 2')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow update of stale locked document - global', async () => {
// Set lock duration to 1 second for testing purposes
const globalConfig = payload.config.globals.find(({ slug }) => slug === menuSlug)
globalConfig.lockDocuments = { duration: 1 }
// Give locking ownership to another user
const lockedGlobalInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
user: {
relationTo: 'users',
value: user2.id,
},
document: undefined,
globalSlug: menuSlug,
},
})
await wait(1100)
const updateGlobalLockedDoc = await payload.updateGlobal({
data: {
globalText: 'global text 2',
},
overrideLock: false,
slug: menuSlug,
})
globalConfig.lockDocuments = { duration: 300 }
// Should allow update since editedAt date is past expiration duration.
// Therefore the document is considered stale
expect(updateGlobalLockedDoc.globalText).toEqual('global text 2')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedGlobalInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedGlobalInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should not allow update of locked document - collection', async () => {
const newPost = await payload.create({
collection: postsSlug,
data: {
text: 'some post',
},
})
// Give locking ownership to another user
await payload.create({
collection: lockedDocumentCollection,
data: {
document: {
relationTo: 'posts',
value: newPost.id,
},
editedAt: new Date().toISOString(),
globalSlug: undefined,
user: {
relationTo: 'users',
value: user2.id,
},
},
})
try {
await payload.update({
collection: postsSlug,
data: {
text: 'updated post',
},
overrideLock: false, // necessary to trigger the lock check
id: newPost.id,
})
} catch (error) {
expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked by another user and cannot be updated/)
}
const updatedPost = await payload.findByID({
collection: postsSlug,
id: newPost.id,
})
// Should not allow update - expect data not to change
expect(updatedPost.text).toEqual('some post')
})
it('should not allow update of locked document - global', async () => {
// Give locking ownership to another user
await payload.create({
collection: lockedDocumentCollection,
data: {
document: undefined,
editedAt: new Date().toISOString(),
globalSlug: menuSlug,
user: {
relationTo: 'users',
value: user2.id,
},
},
})
try {
await payload.updateGlobal({
data: {
globalText: 'updated global text',
},
overrideLock: false, // necessary to trigger the lock check
slug: menuSlug,
})
} catch (error) {
expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked by another user and cannot be updated/)
}
const updatedGlobalMenu = await payload.findGlobal({
slug: menuSlug,
})
// Should not allow update - expect data not to change
expect(updatedGlobalMenu.globalText).toEqual('global text 2')
})
// Try to delete locked document (collection)
it('should not allow delete of locked document - collection', async () => {
const newPost3 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 3',
},
})
// Give locking ownership to another user
await payload.create({
collection: lockedDocumentCollection,
data: {
document: {
relationTo: 'posts',
value: newPost3.id,
},
editedAt: new Date().toISOString(),
globalSlug: undefined,
user: {
relationTo: 'users',
value: user2.id,
},
},
})
try {
await payload.delete({
collection: postsSlug,
id: newPost3.id,
overrideLock: false, // necessary to trigger the lock check
})
} catch (error) {
expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked and cannot be deleted/)
}
const findPostDocs = await payload.find({
collection: postsSlug,
where: {
id: { equals: newPost3.id },
},
})
expect(findPostDocs.docs).toHaveLength(1)
})
it('should allow delete of stale locked document - collection', async () => {
const newPost4 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 4',
},
})
// Set lock duration to 1 second for testing purposes
postConfig.lockDocuments = { duration: 1 }
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost4.id,
},
globalSlug: undefined,
},
})
await wait(1100)
await payload.delete({
collection: postsSlug,
id: newPost4.id,
overrideLock: false,
})
const findPostDocs = await payload.find({
collection: postsSlug,
where: {
id: { equals: newPost4.id },
},
})
expect(findPostDocs.docs).toHaveLength(0)
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow update of locked document w/ overrideLock flag - collection', async () => {
const newPost5 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 5',
},
})
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost5.id,
},
globalSlug: undefined,
},
})
const updateLockedDoc = await payload.update({
collection: postsSlug,
data: {
text: 'updated post 5',
},
id: newPost5.id,
overrideLock: true,
})
// Should allow update since using overrideLock flag
expect(updateLockedDoc.text).toEqual('updated post 5')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow update of locked document w/ overrideLock flag - global', async () => {
// Give locking ownership to another user
const lockedGlobalInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
globalSlug: menuSlug,
user: {
relationTo: 'users',
value: user2.id,
},
document: undefined,
},
})
const updateGlobalLockedDoc = await payload.updateGlobal({
data: {
globalText: 'updated global text 2',
},
slug: menuSlug,
overrideLock: true,
})
// Should allow update since using overrideLock flag
expect(updateGlobalLockedDoc.globalText).toEqual('updated global text 2')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedGlobalInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedGlobalInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow delete of locked document w/ overrideLock flag - collection', async () => {
const newPost6 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 6',
},
})
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost6.id,
},
globalSlug: undefined,
},
})
await payload.delete({
collection: postsSlug,
id: newPost6.id,
overrideLock: true,
})
const findPostDocs = await payload.find({
collection: postsSlug,
where: {
id: { equals: newPost6.id },
},
})
expect(findPostDocs.docs).toHaveLength(0)
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow take over on locked doc (simulates take over modal from admin ui)', async () => {
const newPost7 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 7',
},
})
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost7.id,
},
globalSlug: undefined,
},
})
// This is the take over action - changing the user to the current user
await payload.update({
collection: 'payload-locked-documents',
data: {
user: { relationTo: 'users', value: user?.id },
},
id: lockedDocInstance.id,
})
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
'user.value': { equals: user.id },
},
})
expect(docsFromLocksCollection.docs).toHaveLength(1)
expect(docsFromLocksCollection.docs[0].user.value?.id).toEqual(user.id)
})
})