strapi/tests/api/plugins/content-releases/releases.test.api.ts
Fernando Chávez 5b211b3891
chore(content-releases): releases migration to v5 (#20259)
* chore: migrate bulk publish & unpublish to v5

* chore: change findLocales type to accept arrays

* chore: fix lint error

* chore: migrate bulkDelete to v5 (#20161)

* chore: migrate bulkDelete to v5

* chore: change findLocales type to accept strings array

* fix: docs prettier styles

* chore: remove console.log

* enhancement: migrate countManyDraftRelations to v5

* chore(content-releases): v5 migration

* chore(content-releases): remove CMReleasesContainer

* fix(content-releases): singleType works with v5 changes and e2e tests enabled

* fix(content-releases): apply josh & marc comments

* apply comments

* fix(content-releases): tests

* fix(content-releases): create custom populate object for each content type to handle relations

* fix(content-releases): build problem

* fix(content-releases): editing lifecycles

* fix(content-releases): details view table columns

* feat: releases settings (#20354)

* feat: releases settings

* feat: test nulling default timezone

* chore: refactor tests

* fix: remove async from describe

* chore: OneOf type for response

* chore: move OneOf utility to types package

---------

Co-authored-by: Fernando Chavez <fernando.chavez@strapi.io>

* feat: content releases settings permissions (#20357)

* feat: releases settings

* feat: test nulling default timezone

* chore: refactor tests

* fix: remove async from describe

* feat: content releases settings permissions

* chore: test for unauthorized role

---------

Co-authored-by: Fernando Chavez <fernando.chavez@strapi.io>

* fix(content-releases): run settings api tests only on ee edition

* fix(content-releases): apply mark comments

* fix(content-releases): remove releases box when there are no releases related to the entry

* fix(content-releases): relation between actions and documents (#20424)

* fix(content-releases): refactor relation with entries

* fix(content-releases): refactor relation with entries

* fix(content-releases): lint & unit tests errors

* fix(content-releases): add migration for releases actions coming from v4

* fix(content-releases): apply multiple suggestions

* fix(content-releases): new test data for e2e tests

* fix(content-releases): fix test data

* fix(content-releases): handle edge cases

* fix(content-releases): apply marc suggestions

* fix(content-releases): add modified status on validation column

* fix(content-releases): fix releases menu button

* fix(content-releases): use documents middleware instead of db lifecycles

* fix(content-releases): invalidate releases tags on some content manager queries

* fix(content-releases): using contentType utils and make afterDeleteMany lifecycle async

* fix(content-releases): ui fixs

* fix(content-releases): removing not needed axios from releases plugin

* fix(content-releases): invalidate tags on release service

* fix(content-releases): fix dependencies

* feat(release-settings): remove navbar link release purchase page in CE (#20498)

---------

Co-authored-by: Marc Roig <marc.roig.campos@strapi.io>
Co-authored-by: Simone <startae14@gmail.com>
2024-06-13 11:12:38 +02:00

712 lines
20 KiB
TypeScript

'use strict';
import { createStrapiInstance } from 'api-tests/strapi';
import { createAuthRequest } 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,
},
},
};
// Skip all releases tests for now until releases is migrated to v5
describeOnCondition(false /* edition === 'EE' */)('Content Releases API', () => {
const builder = createTestBuilder();
let strapi;
let rq;
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, entryId, type, entryType = 'collection-types' }
) => {
return rq({
method: 'POST',
url: `/content-releases/${releaseId}/actions`,
body: {
entryDocumentId: entryId,
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).build();
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z'));
// Create products
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' }),
]);
});
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(200);
});
test('cannot create a release with the same name', async () => {
const firstCreateRes = await createRelease();
expect(firstCreateRes.statusCode).toBe(200);
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(200);
});
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(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
expect(createActionRes.statusCode).toBe(200);
expect(createActionRes.body.data.isEntryValid).toBe(true);
});
test('Create a release action with invalid status', async () => {
const createReleaseRes = await createRelease();
expect(createReleaseRes.statusCode).toBe(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 5,
type: 'publish',
});
expect(createActionRes.statusCode).toBe(200);
expect(createActionRes.body.data.isEntryValid).toBe(false);
});
test('cannot create an action with invalid contentTypeUid', async () => {
const createReleaseRes = await createRelease();
expect(createReleaseRes.statusCode).toBe(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: 'invalid',
entryId: 1,
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,
entryId: 1,
type: 'publish',
});
expect(firstCreateActionRes.statusCode).toBe(200);
const secondCreateActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
expect(secondCreateActionRes.statusCode).toBe(400);
expect(secondCreateActionRes.body.error.message).toBe(
`Entry with id 1 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(200);
const release = createReleaseRes.body.data;
const res = await rq({
method: 'POST',
url: `/content-releases/${release.id}/actions/bulk`,
body: [
{
entry: {
id: 3,
contentType: productUID,
},
type: 'publish',
},
{
entry: {
id: 4,
contentType: productUID,
},
type: 'publish',
},
],
});
expect(res.statusCode).toBe(200);
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(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 4,
type: 'publish',
});
expect(createActionRes.statusCode).toBe(200);
const res = await rq({
method: 'POST',
url: `/content-releases/${release.id}/actions/bulk`,
body: [
{
type: 'publish',
entry: {
id: 4,
contentType: productUID,
},
},
{
entry: {
id: 5,
contentType: productUID,
},
type: 'publish',
},
],
});
expect(res.statusCode).toBe(200);
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(200);
const release = createReleaseRes.body.data;
await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
await createReleaseAction(release.id, {
contentType: productUID,
entryId: 2,
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(200);
const release = createReleaseRes.body.data;
await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
await createReleaseAction(release.id, {
contentType: productUID,
entryId: 2,
type: 'publish',
});
await createReleaseAction(release.id, {
contentType: productUID,
entryId: 3,
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(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
expect(createActionRes.statusCode).toBe(200);
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,
entryId: 1,
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(200);
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(200);
const release = createReleaseRes.body.data;
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 1,
type: 'publish',
});
expect(createActionRes.statusCode).toBe(200);
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(200);
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,
entryId: 1,
type: 'publish',
});
const createActionRes = await createReleaseAction(release.id, {
contentType: productUID,
entryId: 5,
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(200);
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,
entryId: 1,
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,
entryId: 1,
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,
entryId: 5,
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`.'
);
});
});
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,
entryId: 1,
type: 'publish',
});
const res = await rq({
method: 'GET',
url: `/content-releases?contentTypeUid=${productUID}&entryId=1&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?contentTypeUid=${productUID}&entryId=1&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);
});
// @TODO: This test is failing on CI for sqlite with node20 apparently with no reason
test.skip('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');
});
});
});