/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
tests/api/plugins/content-releases/releases.test.api.ts
911 строк
28 KB
Bassel Kanso
Fix: 25780 release relation sync (#26700)
19 июн 2026, 17:43
Не верифицирован
19 июн 2026, 17:43
2ba3a00
Код
Авторство
О чём код?
'use strict'; import { createStrapiInstance } from 'api-tests/strapi'; import { createAuthRequest, createContentAPIRequest } from 'api-tests/request'; import { describeOnCondition } from 'api-tests/utils'; import { createTestBuilder } from 'api-tests/builder'; import { CreateRelease } from '../../../../packages/core/content-releases/shared/contracts/releases'; const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE'; const productUID = 'api::product.product'; const productModel = { draftAndPublish: true, pluginOptions: {}, singularName: 'product', pluralName: 'products', displayName: 'Product', kind: 'collectionType', attributes: { name: { type: 'string', }, description: { type: 'string', required: true, }, }, }; const categoryUID = 'api::category.category'; const categoryModel = { draftAndPublish: true, pluginOptions: {}, singularName: 'category', pluralName: 'categories', displayName: 'Category', kind: 'collectionType', attributes: { name: { type: 'string' }, }, }; const articleUID = 'api::article.article'; const articleModel = { draftAndPublish: true, pluginOptions: {}, singularName: 'article', pluralName: 'articles', displayName: 'Article', kind: 'collectionType', attributes: { title: { type: 'string' }, categories: { type: 'relation', relation: 'manyToMany', target: 'api::category.category', }, }, }; const pageUID = 'api::page.page'; const pageModel = { draftAndPublish: true, pluginOptions: {}, singularName: 'page', pluralName: 'pages', displayName: 'Page', kind: 'collectionType', attributes: { title: { type: 'string' }, parent: { type: 'relation', relation: 'manyToOne', target: 'api::page.page', targetAttribute: 'children', }, }, }; describeOnCondition(edition === 'EE')('Content Releases API', () => { const builder = createTestBuilder(); let strapi; let rq; let rqContent; let validEntries = []; let invalidEntries = []; const createRelease = async (params: Partial<CreateRelease.Request['body']> = {}) => { return rq({ method: 'POST', url: '/content-releases/', body: { name: `Test Release ${Math.random().toString(36)}`, scheduledAt: null, ...params, }, }); }; const createReleaseAction = async (releaseId, { contentType, entryDocumentId, type }) => { return rq({ method: 'POST', url: `/content-releases/${releaseId}/actions`, body: { entryDocumentId, contentType, type, }, }); }; const deleteAllReleases = async () => { const releases = await rq({ method: 'GET', url: '/content-releases', }); await Promise.all( releases.body.data.map(async (release) => { await rq({ method: 'DELETE', url: `/content-releases/${release.id}`, }); }) ); }; const createEntry = async (uid, data) => { const { body } = await rq({ method: 'POST', url: `/content-manager/collection-types/${uid}`, body: data, }); return body; }; beforeAll(async () => { await builder .addContentType(productModel) .addContentTypes([categoryModel, articleModel, pageModel]) .build(); strapi = await createStrapiInstance(); rq = await createAuthRequest({ strapi }); rqContent = createContentAPIRequest({ strapi }); jest.useFakeTimers(); jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); // Create products validEntries = await Promise.all([ createEntry(productUID, { name: 'Product 1', description: 'Description' }), createEntry(productUID, { name: 'Product 2', description: 'Description' }), createEntry(productUID, { name: 'Product 3', description: 'Description' }), createEntry(productUID, { name: 'Product 4', description: 'Description' }), createEntry(productUID, { name: 'Invalid Product' }), ]); invalidEntries = await Promise.all([createEntry(productUID, { name: 'Invalid Product' })]); }); beforeEach(async () => { await deleteAllReleases(); }); afterAll(async () => { jest.useRealTimers(); await strapi.destroy(); await builder.cleanup(); }); describe('Create Release', () => { test('Create a release', async () => { const res = await createRelease(); expect(res.statusCode).toBe(201); }); test('cannot create a release with the same name', async () => { const firstCreateRes = await createRelease(); expect(firstCreateRes.statusCode).toBe(201); const releaseName = firstCreateRes.body.data.name; const secondCreateRes = await createRelease({ name: releaseName }); expect(secondCreateRes.body.error.message).toBe( `Release with name ${releaseName} already exists` ); }); test('create a scheduled release', async () => { const res = await createRelease({ scheduledAt: new Date('2024-10-10T00:00:00.000Z'), timezone: 'Europe/Madrid', }); expect(res.statusCode).toBe(201); }); test('cannot create a scheduled release with date in the past', async () => { const res = await createRelease({ scheduledAt: new Date('2022-10-10T00:00:00.000Z'), timezone: 'Europe/Madrid', }); expect(res.statusCode).toBe(400); expect(res.body.error.message).toBe('Scheduled at must be later than now'); }); }); describe('Create Release Actions', () => { test('Create a release action with valid status', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(201); expect(createActionRes.body.data.isEntryValid).toBe(true); }); test('Create a release action with invalid status', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: invalidEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(201); expect(createActionRes.body.data.isEntryValid).toBe(false); }); test('cannot create an action with invalid contentTypeUid', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: 'invalid', entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(404); expect(createActionRes.body.error.message).toBe('No content type found for uid invalid'); }); test('throws an error when trying to add an entry that is already in the release', async () => { const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; const firstCreateActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(firstCreateActionRes.statusCode).toBe(201); const secondCreateActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(secondCreateActionRes.statusCode).toBe(400); expect(secondCreateActionRes.body.error.message).toBe( `Entry with documentId ${validEntries[0].data.documentId} and contentType api::product.product already exists in release with id ${release.id}` ); }); }); describe('Create Many Release Actions', () => { test('Create many release actions', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const res = await rq({ method: 'POST', url: `/content-releases/${release.id}/actions/bulk`, body: [ { entryDocumentId: validEntries[0].data.documentId, contentType: productUID, type: 'publish', }, { entryDocumentId: validEntries[1].data.documentId, contentType: productUID, type: 'publish', }, ], }); expect(res.statusCode).toBe(201); expect(res.body.meta.entriesAlreadyInRelease).toBe(0); expect(res.body.meta.totalEntries).toBe(2); }); test('If entry is already in the release, it should not be added', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(201); const res = await rq({ method: 'POST', url: `/content-releases/${release.id}/actions/bulk`, body: [ { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }, { contentType: productUID, entryDocumentId: validEntries[1].data.documentId, type: 'publish', }, ], }); expect(res.statusCode).toBe(201); expect(res.body.meta.entriesAlreadyInRelease).toBe(1); expect(res.body.meta.totalEntries).toBe(2); }); }); describe('Find Many Release Actions', () => { test('Find many release actions', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[1].data.documentId, type: 'publish', }); const res = await rq({ method: 'GET', url: `/content-releases/${release.id}/actions`, }); expect(res.statusCode).toBe(200); expect(res.body.data.Product.length).toBe(2); }); test('Group by action type', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[1].data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[2].data.documentId, type: 'unpublish', }); const res = await rq({ method: 'GET', url: `/content-releases/${release.id}/actions?groupBy=action`, }); expect(res.statusCode).toBe(200); expect(res.body.data.publish.length).toBe(2); expect(res.body.data.unpublish.length).toBe(1); }); }); describe('Edit Release Action', () => { test('Edit a release action', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(201); const releaseAction = createActionRes.body.data; const changeToUnpublishRes = await rq({ method: 'PUT', url: `/content-releases/${release.id}/actions/${releaseAction.id}`, body: { type: 'unpublish', }, }); expect(changeToUnpublishRes.statusCode).toBe(200); expect(changeToUnpublishRes.body.data.type).toBe('unpublish'); const changeToPublishRes = await rq({ method: 'PUT', url: `/content-releases/${release.id}/actions/${releaseAction.id}`, body: { type: 'publish', }, }); expect(changeToPublishRes.statusCode).toBe(200); expect(changeToPublishRes.body.data.type).toBe('publish'); }); }); describe('Delete a Release Action', () => { test('Delete a release action', async () => { const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); const releaseAction = createActionRes.body.data; const res = await rq({ method: 'DELETE', url: `/content-releases/${release.id}/actions/${releaseAction.id}`, }); expect(res.statusCode).toBe(200); const findRes = await rq({ method: 'GET', url: `/content-releases/${release.id}/actions`, }); expect(findRes.statusCode).toBe(200); }); test('cannot delete a release action that does not exist', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const res = await rq({ method: 'DELETE', url: `/content-releases/${release.id}/actions/1`, }); expect(res.statusCode).toBe(404); expect(res.body.error.message).toBe( `Action with id 1 not found in release with id ${release.id} or it is already published` ); }); }); describe('Find One Release', () => { test('Find a release', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); expect(createActionRes.statusCode).toBe(201); const res = await rq({ method: 'GET', url: `/content-releases/${release.id}`, }); expect(res.statusCode).toBe(200); expect(res.body.data.name).toBe(release.name); expect(res.body.data.status).toBe('ready'); }); test('Release status is empty if doesnt have any actions', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const res = await rq({ method: 'GET', url: `/content-releases/${release.id}`, }); expect(res.statusCode).toBe(200); expect(res.body.data.name).toBe(release.name); expect(res.body.data.status).toBe('empty'); }); test('Release status is blocked if at least one action is invalid and then change to ready if removed', async () => { const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); const createActionRes = await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: invalidEntries[0].data.documentId, type: 'publish', }); const releaseAction = createActionRes.body.data; const findBlockedRes = await rq({ method: 'GET', url: `/content-releases/${release.id}`, }); expect(findBlockedRes.statusCode).toBe(200); expect(findBlockedRes.body.data.name).toBe(release.name); expect(findBlockedRes.body.data.status).toBe('blocked'); const removeEntryRes = await rq({ method: 'DELETE', url: `/content-releases/${release.id}/actions/${releaseAction.id}`, }); expect(removeEntryRes.statusCode).toBe(200); const findReadyRes = await rq({ method: 'GET', url: `/content-releases/${release.id}`, }); expect(findReadyRes.statusCode).toBe(200); expect(findReadyRes.body.data.name).toBe(release.name); expect(findReadyRes.body.data.status).toBe('ready'); }); }); describe('Edit Release', () => { test('Edit a release', async () => { const createReleaseRes = await createRelease(); expect(createReleaseRes.statusCode).toBe(201); const release = createReleaseRes.body.data; const res = await rq({ method: 'PUT', url: `/content-releases/${release.id}`, body: { name: 'Updated Release', }, }); expect(res.statusCode).toBe(200); expect(res.body.data.name).toBe('Updated Release'); }); test('cannot change to a name that already exists', async () => { const createFirstReleaseRes = await createRelease(); const createSecondReleaseRes = await createRelease(); const res = await rq({ method: 'PUT', url: `/content-releases/${createFirstReleaseRes.body.data.id}`, body: { name: createSecondReleaseRes.body.data.name, }, }); expect(res.statusCode).toBe(400); expect(res.body.error.message).toBe( `Release with name ${createSecondReleaseRes.body.data.name} already exists` ); }); }); describe('Publish Release', () => { test('Publish a release', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); const res = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(res.statusCode).toBe(200); expect(res.body.data.status).toBe('done'); }); test('cannot publish a release that is already published', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[0].data.documentId, type: 'publish', }); const firstPublishRes = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(firstPublishRes.statusCode).toBe(200); expect(firstPublishRes.body.data.status).toBe('done'); const secondPublishRes = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(secondPublishRes.statusCode).toBe(400); expect(secondPublishRes.body.error.message).toBe('Release already published'); }); test('cannot publish a release if at least one action is invalid', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: invalidEntries[0].data.documentId, type: 'publish', }); const res = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(res.statusCode).toBe(400); expect(res.body.error.message).toBe( 'description must be a `string` type, but the final value was: `null`.' ); }); test('retrieves relations correctly in content API after publishing release', async () => { const categoryEntry = await createEntry(categoryUID, { name: 'Tech' }); const articleEntry = await createEntry(articleUID, { title: 'My Article', categories: [{ documentId: categoryEntry.data.documentId }], }); const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: categoryUID, entryDocumentId: categoryEntry.data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: articleUID, entryDocumentId: articleEntry.data.documentId, type: 'publish', }); const publishRes = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(publishRes.statusCode).toBe(200); expect(publishRes.body.data.status).toBe('done'); const apiRes = await rqContent({ method: 'GET', url: `/articles`, qs: { populate: 'categories' }, }); expect(apiRes.statusCode).toBe(200); expect(apiRes.body.data).toHaveLength(1); const article = apiRes.body.data[0]; expect(article.attributes?.title ?? article.title).toBe('My Article'); const categories = article.attributes?.categories?.data ?? article.attributes?.categories ?? article.categories; expect(categories).toBeDefined(); const categoryList = Array.isArray(categories) ? categories : [categories]; expect(categoryList).toHaveLength(1); const categoryData = categoryList[0]; const categoryName = categoryData.attributes?.name ?? categoryData.name; expect(categoryName).toBe('Tech'); }); test('publishes a release when a self-relation target was modified between related sources', async () => { const parentPage = await createEntry(pageUID, { title: 'Initial Parent Page' }); await strapi.documents(pageUID).publish({ documentId: parentPage.data.documentId }); const firstChildPage = await createEntry(pageUID, { title: 'First child with modified relation target', parent: { documentId: parentPage.data.documentId, locale: null }, }); const secondChildPage = await createEntry(pageUID, { title: 'Second child with modified relation target', parent: { documentId: parentPage.data.documentId, locale: null }, }); const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: pageUID, entryDocumentId: firstChildPage.data.documentId, type: 'publish', }); await strapi.documents(pageUID).update({ documentId: parentPage.data.documentId, data: { title: 'Updated Parent Page' }, }); await createReleaseAction(release.id, { contentType: pageUID, entryDocumentId: parentPage.data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: pageUID, entryDocumentId: secondChildPage.data.documentId, type: 'publish', }); const publishRes = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(publishRes.statusCode).toBe(200); expect(publishRes.body.data.status).toBe('done'); const firstPublishedChildRes = await rq({ method: 'GET', url: `/content-manager/collection-types/${pageUID}/${firstChildPage.data.documentId}`, qs: { status: 'published' }, }); const secondPublishedChildRes = await rq({ method: 'GET', url: `/content-manager/collection-types/${pageUID}/${secondChildPage.data.documentId}`, qs: { status: 'published' }, }); expect(firstPublishedChildRes.statusCode).toBe(200); expect(firstPublishedChildRes.body.data.parent).toMatchObject({ count: 1 }); expect(secondPublishedChildRes.statusCode).toBe(200); expect(secondPublishedChildRes.body.data.parent).toMatchObject({ count: 1 }); }); test('publishes self-related parent and child in one release and preserves relation', async () => { const parentPage = await createEntry(pageUID, { title: 'Parent Page' }); const childPage = await createEntry(pageUID, { title: 'Child Page', parent: { documentId: parentPage.data.documentId, locale: null }, }); const createReleaseRes = await createRelease(); const release = createReleaseRes.body.data; await createReleaseAction(release.id, { contentType: pageUID, entryDocumentId: parentPage.data.documentId, type: 'publish', }); await createReleaseAction(release.id, { contentType: pageUID, entryDocumentId: childPage.data.documentId, type: 'publish', }); const publishRes = await rq({ method: 'POST', url: `/content-releases/${release.id}/publish`, }); expect(publishRes.statusCode).toBe(200); expect(publishRes.body.data.status).toBe('done'); const publishedChildRes = await rq({ method: 'GET', url: `/content-manager/collection-types/${pageUID}/${childPage.data.documentId}`, qs: { status: 'published' }, }); expect(publishedChildRes.statusCode).toBe(200); expect(publishedChildRes.body.data.parent).toMatchObject({ count: 1 }); }); }); describe('Find Many Releases', () => { test('Find many not published releases', async () => { await createRelease(); await createRelease(); const res = await rq({ method: 'GET', url: '/content-releases?filters[releasedAt][$notNull]=false', }); expect(res.statusCode).toBe(200); expect(res.body.data.length).toBe(2); expect(res.body.meta.pendingReleasesCount).toBe(2); }); test('Find many releases with an entry attached', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; await createReleaseAction(release.id, { contentType: productUID, entryDocumentId: validEntries[3].data.documentId, type: 'publish', }); const res = await rq({ method: 'GET', url: `/content-releases/getByDocumentAttached?contentType=${productUID}&entryDocumentId=${validEntries[3].data.documentId}&hasEntryAttached=true`, }); expect(res.statusCode).toBe(200); expect(res.body.data.length).toBe(1); expect(res.body.data[0].name).toBe(release.name); }); test('Find many releases without an entry attached', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; const res = await rq({ method: 'GET', url: `/content-releases/getByDocumentAttached?contentType=${productUID}&entryDocumentId=${validEntries[4]}&hasEntryAttached=false`, }); expect(res.statusCode).toBe(200); expect(res.body.data.length).toBe(1); expect(res.body.data[0].name).toBe(release.name); }); }); describe('Delete Release', () => { test('Delete a release', async () => { const createFirstReleaseRes = await createRelease(); const release = createFirstReleaseRes.body.data; const res = await rq({ method: 'DELETE', url: `/content-releases/${release.id}`, }); expect(res.statusCode).toBe(200); }); test('cannot delete a release that does not exist', async () => { const res = await rq({ method: 'DELETE', url: '/content-releases/999', }); expect(res.statusCode).toBe(404); expect(res.body.error.message).toBe('No release found for id 999'); }); }); });