mirror of
https://github.com/strapi/strapi.git
synced 2025-12-19 11:13:58 +00:00
chore(history): add api tests (#20157)
This commit is contained in:
parent
7431ba9b38
commit
bdaafbbb3c
@ -45,18 +45,18 @@ export const VersionHeader = ({ headerId }: VersionHeaderProps) => {
|
|||||||
|
|
||||||
const mainFieldValue = version.data[mainField];
|
const mainFieldValue = version.data[mainField];
|
||||||
|
|
||||||
const getBackLink = (): To => {
|
const getNextNavigation = (): To => {
|
||||||
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });
|
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });
|
||||||
|
|
||||||
if (collectionType === COLLECTION_TYPES) {
|
if (collectionType === COLLECTION_TYPES) {
|
||||||
return {
|
return {
|
||||||
pathname: `../${collectionType}/${version.contentType}/${version.relatedDocumentId}`,
|
pathname: `/content-manager/${collectionType}/${version.contentType}/${version.relatedDocumentId}`,
|
||||||
search: pluginsQueryParams,
|
search: pluginsQueryParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pathname: `../${collectionType}/${version.contentType}`,
|
pathname: `/content-manager/${collectionType}/${version.contentType}`,
|
||||||
search: pluginsQueryParams,
|
search: pluginsQueryParams,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -64,16 +64,17 @@ export const VersionHeader = ({ headerId }: VersionHeaderProps) => {
|
|||||||
const handleRestore = async () => {
|
const handleRestore = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await restoreVersion({
|
const response = await restoreVersion({
|
||||||
|
documentId: version.relatedDocumentId,
|
||||||
|
collectionType,
|
||||||
params: {
|
params: {
|
||||||
versionId: version.id,
|
versionId: version.id,
|
||||||
documentId: version.relatedDocumentId,
|
|
||||||
contentType: version.contentType,
|
contentType: version.contentType,
|
||||||
},
|
},
|
||||||
body: { contentType: version.contentType },
|
body: { contentType: version.contentType },
|
||||||
});
|
});
|
||||||
|
|
||||||
if ('data' in response) {
|
if ('data' in response) {
|
||||||
navigate(`/content-manager/${collectionType}/${slug}/${response.data.data?.documentId}`);
|
navigate(getNextNavigation());
|
||||||
|
|
||||||
toggleNotification({
|
toggleNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -137,7 +138,7 @@ export const VersionHeader = ({ headerId }: VersionHeaderProps) => {
|
|||||||
startIcon={<ArrowLeft />}
|
startIcon={<ArrowLeft />}
|
||||||
as={NavLink}
|
as={NavLink}
|
||||||
// @ts-expect-error - types are not inferred correctly through the as prop.
|
// @ts-expect-error - types are not inferred correctly through the as prop.
|
||||||
to={getBackLink()}
|
to={getNextNavigation()}
|
||||||
>
|
>
|
||||||
{formatMessage({
|
{formatMessage({
|
||||||
id: 'global.back',
|
id: 'global.back',
|
||||||
|
|||||||
@ -80,7 +80,7 @@ describe('VersionHeader', () => {
|
|||||||
const backLink = screen.getByRole('link', { name: 'Back' });
|
const backLink = screen.getByRole('link', { name: 'Back' });
|
||||||
expect(backLink).toHaveAttribute(
|
expect(backLink).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'/collection-types/api::kitchensink.kitchensink/pcwmq3rlmp5w0be3cuplhnpr'
|
'/content-manager/collection-types/api::kitchensink.kitchensink/pcwmq3rlmp5w0be3cuplhnpr'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ describe('VersionHeader', () => {
|
|||||||
const backLink = screen.getByRole('link', { name: 'Back' });
|
const backLink = screen.getByRole('link', { name: 'Back' });
|
||||||
expect(backLink).toHaveAttribute(
|
expect(backLink).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'/collection-types/api::kitchensink.kitchensink/pcwmq3rlmp5w0be3cuplhnpr?plugins[i18n][locale]=en'
|
'/content-manager/collection-types/api::kitchensink.kitchensink/pcwmq3rlmp5w0be3cuplhnpr?plugins[i18n][locale]=en'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -158,7 +158,10 @@ describe('VersionHeader', () => {
|
|||||||
expect(await screen.findByText('Test Title (homepage)')).toBeInTheDocument();
|
expect(await screen.findByText('Test Title (homepage)')).toBeInTheDocument();
|
||||||
|
|
||||||
const backLink = screen.getByRole('link', { name: 'Back' });
|
const backLink = screen.getByRole('link', { name: 'Back' });
|
||||||
expect(backLink).toHaveAttribute('href', '/single-types/api::homepage.homepage');
|
expect(backLink).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/content-manager/single-types/api::homepage.homepage'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the correct title and subtitle for a localized entry', async () => {
|
it('should display the correct title and subtitle for a localized entry', async () => {
|
||||||
@ -185,7 +188,7 @@ describe('VersionHeader', () => {
|
|||||||
const backLink = screen.getByRole('link', { name: 'Back' });
|
const backLink = screen.getByRole('link', { name: 'Back' });
|
||||||
expect(backLink).toHaveAttribute(
|
expect(backLink).toHaveAttribute(
|
||||||
'href',
|
'href',
|
||||||
'/single-types/api::homepage.homepage?plugins[i18n][locale]=en'
|
'/content-manager/single-types/api::homepage.homepage?plugins[i18n][locale]=en'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
|
import { Data } from '@strapi/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GetHistoryVersions,
|
GetHistoryVersions,
|
||||||
RestoreHistoryVersion,
|
RestoreHistoryVersion,
|
||||||
} from '../../../../shared/contracts/history-versions';
|
} from '../../../../shared/contracts/history-versions';
|
||||||
|
import { COLLECTION_TYPES } from '../../constants/collections';
|
||||||
import { contentManagerApi } from '../../services/api';
|
import { contentManagerApi } from '../../services/api';
|
||||||
|
|
||||||
|
interface RestoreVersion extends RestoreHistoryVersion.Request {
|
||||||
|
documentId: Data.ID;
|
||||||
|
collectionType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const historyVersionsApi = contentManagerApi.injectEndpoints({
|
const historyVersionsApi = contentManagerApi.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getHistoryVersions: builder.query<
|
getHistoryVersions: builder.query<
|
||||||
@ -21,23 +29,27 @@ const historyVersionsApi = contentManagerApi.injectEndpoints({
|
|||||||
},
|
},
|
||||||
providesTags: ['HistoryVersion'],
|
providesTags: ['HistoryVersion'],
|
||||||
}),
|
}),
|
||||||
restoreVersion: builder.mutation<RestoreHistoryVersion.Response, RestoreHistoryVersion.Request>(
|
restoreVersion: builder.mutation<RestoreHistoryVersion.Response, RestoreVersion>({
|
||||||
{
|
query({ params, body }) {
|
||||||
query({ params, body }) {
|
return {
|
||||||
return {
|
url: `/content-manager/history-versions/${params.versionId}/restore`,
|
||||||
url: `/content-manager/history-versions/${params.versionId}/restore`,
|
method: 'PUT',
|
||||||
method: 'PUT',
|
data: body,
|
||||||
data: body,
|
};
|
||||||
};
|
},
|
||||||
},
|
invalidatesTags: (_res, _error, { documentId, collectionType, params }) => {
|
||||||
invalidatesTags: (_res, _error, { params }) => {
|
return [
|
||||||
return [
|
'HistoryVersion',
|
||||||
'HistoryVersion',
|
{
|
||||||
{ type: 'Document', id: `${params.contentType}_${params.documentId}` },
|
type: 'Document',
|
||||||
];
|
id:
|
||||||
},
|
collectionType === COLLECTION_TYPES
|
||||||
}
|
? `${params.contentType}_${documentId}`
|
||||||
),
|
: params.contentType,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,199 +0,0 @@
|
|||||||
import { createHistoryVersionController } from '../history-version';
|
|
||||||
|
|
||||||
const mockFindVersionsPage = jest.fn();
|
|
||||||
|
|
||||||
// History utils
|
|
||||||
jest.mock('../../utils', () => ({
|
|
||||||
getService: jest.fn((_strapi, name) => {
|
|
||||||
if (name === 'history') {
|
|
||||||
return {
|
|
||||||
findVersionsPage: mockFindVersionsPage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Content Manager utils
|
|
||||||
jest.mock('../../../utils', () => ({
|
|
||||||
getService: jest.fn((name) => {
|
|
||||||
if (name === 'permission-checker') {
|
|
||||||
return {
|
|
||||||
create: jest.fn(() => ({
|
|
||||||
cannot: {
|
|
||||||
read: jest.fn(() => false),
|
|
||||||
},
|
|
||||||
sanitizeQuery: jest.fn((query) => query),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('History version controller', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFindVersionsPage.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findMany', () => {
|
|
||||||
it('should require contentType and documentId for collection types', () => {
|
|
||||||
const ctx = {
|
|
||||||
state: {
|
|
||||||
userAbility: {},
|
|
||||||
},
|
|
||||||
query: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const historyVersionController = createHistoryVersionController({
|
|
||||||
// @ts-expect-error - we're not mocking the entire strapi object
|
|
||||||
strapi: { getModel: jest.fn(() => ({ kind: 'collectionType' })) },
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
expect(historyVersionController.findMany(ctx)).rejects.toThrow(
|
|
||||||
/contentType and documentId are required/
|
|
||||||
);
|
|
||||||
expect(mockFindVersionsPage).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require contentType for single types', () => {
|
|
||||||
const ctx = {
|
|
||||||
state: {
|
|
||||||
userAbility: {},
|
|
||||||
},
|
|
||||||
query: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const historyVersionController = createHistoryVersionController({
|
|
||||||
// @ts-expect-error - we're not mocking the entire strapi object
|
|
||||||
strapi: { getModel: jest.fn(() => ({ kind: 'singleType' })) },
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
expect(historyVersionController.findMany(ctx)).rejects.toThrow(/contentType is required/);
|
|
||||||
expect(mockFindVersionsPage).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call findVersionsPage for collection types', async () => {
|
|
||||||
const ctx = {
|
|
||||||
state: {
|
|
||||||
userAbility: {},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
documentId: 'document-id',
|
|
||||||
contentType: 'api::test.test',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockFindVersionsPage.mockResolvedValueOnce({
|
|
||||||
results: [{ id: 'history-version-id' }],
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
pageCount: 1,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const historyVersionController = createHistoryVersionController({
|
|
||||||
// @ts-expect-error - we're not mocking the entire strapi object
|
|
||||||
strapi: { getModel: jest.fn(() => ({ kind: 'collectionType' })) },
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
const response = await historyVersionController.findMany(ctx);
|
|
||||||
|
|
||||||
expect(mockFindVersionsPage).toHaveBeenCalled();
|
|
||||||
expect(response.data.length).toBe(1);
|
|
||||||
expect(response.meta.pagination).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call findVersionsPage for single types', async () => {
|
|
||||||
const ctx = {
|
|
||||||
state: {
|
|
||||||
userAbility: {},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
contentType: 'api::test.test',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockFindVersionsPage.mockResolvedValueOnce({
|
|
||||||
results: [{ id: 'history-version-id' }],
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
pageCount: 1,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const historyVersionController = createHistoryVersionController({
|
|
||||||
// @ts-expect-error - we're not mocking the entire strapi object
|
|
||||||
strapi: { getModel: jest.fn(() => ({ kind: 'singleType' })) },
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
const response = await historyVersionController.findMany(ctx);
|
|
||||||
|
|
||||||
expect(mockFindVersionsPage).toHaveBeenCalled();
|
|
||||||
expect(response.data.length).toBe(1);
|
|
||||||
expect(response.meta.pagination).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies pagination params', async () => {
|
|
||||||
const ctx = {
|
|
||||||
state: {
|
|
||||||
userAbility: {},
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
contentType: 'api::test.test',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const historyVersionController = createHistoryVersionController({
|
|
||||||
// @ts-expect-error - we're not mocking the entire strapi object
|
|
||||||
strapi: { getModel: jest.fn(() => ({ kind: 'singleType' })) },
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies default pagination params
|
|
||||||
*/
|
|
||||||
mockFindVersionsPage.mockResolvedValueOnce({
|
|
||||||
results: [],
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
const mockResponse = await historyVersionController.findMany(ctx);
|
|
||||||
expect(mockFindVersionsPage).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(mockResponse.meta.pagination.page).toBe(1);
|
|
||||||
expect(mockResponse.meta.pagination.pageSize).toBe(20);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevents invalid pagination params
|
|
||||||
*/
|
|
||||||
mockFindVersionsPage.mockResolvedValueOnce({
|
|
||||||
results: [],
|
|
||||||
pagination: {},
|
|
||||||
});
|
|
||||||
// @ts-expect-error partial context
|
|
||||||
await historyVersionController.findMany({
|
|
||||||
...ctx,
|
|
||||||
query: { ...ctx.query, page: '-1', pageSize: '1000' },
|
|
||||||
});
|
|
||||||
expect(mockFindVersionsPage).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -37,13 +37,13 @@ const createHistoryVersionController = ({ strapi }: { strapi: Core.Strapi }) =>
|
|||||||
return {
|
return {
|
||||||
async findMany(ctx) {
|
async findMany(ctx) {
|
||||||
const contentTypeUid = ctx.query.contentType as UID.ContentType;
|
const contentTypeUid = ctx.query.contentType as UID.ContentType;
|
||||||
const isSingleType = strapi.getModel(contentTypeUid).kind === 'singleType';
|
const isSingleType = strapi.getModel(contentTypeUid)?.kind === 'singleType';
|
||||||
|
|
||||||
if (isSingleType && !contentTypeUid) {
|
if (isSingleType && !contentTypeUid) {
|
||||||
throw new errors.ForbiddenError('contentType is required');
|
throw new errors.ForbiddenError('contentType is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!contentTypeUid && !ctx.query.documentId) {
|
if (!isSingleType && (!contentTypeUid || !ctx.query.documentId)) {
|
||||||
throw new errors.ForbiddenError('contentType and documentId are required');
|
throw new errors.ForbiddenError('contentType and documentId are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,9 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const localesService = strapi.plugin('i18n')?.service('locales');
|
const localesService = strapi.plugin('i18n')?.service('locales');
|
||||||
|
|
||||||
|
const getDefaultLocale = async () => (localesService ? localesService.getDefaultLocale() : null);
|
||||||
|
|
||||||
const getLocaleDictionary = async () => {
|
const getLocaleDictionary = async () => {
|
||||||
if (!localesService) return {};
|
if (!localesService) return {};
|
||||||
|
|
||||||
@ -163,8 +166,9 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
|
|||||||
? { documentId: result.documentId, locale: context.params?.locale }
|
? { documentId: result.documentId, locale: context.params?.locale }
|
||||||
: { documentId: context.params.documentId, locale: context.params?.locale };
|
: { documentId: context.params.documentId, locale: context.params?.locale };
|
||||||
|
|
||||||
const defaultLocale = localesService ? await localesService.getDefaultLocale() : null;
|
const defaultLocale = await getDefaultLocale();
|
||||||
const locale = documentContext.locale || defaultLocale;
|
const locale = documentContext.locale || defaultLocale;
|
||||||
|
|
||||||
const document = await strapi.documents(contentTypeUid).findOne({
|
const document = await strapi.documents(contentTypeUid).findOne({
|
||||||
documentId: documentContext.documentId,
|
documentId: documentContext.documentId,
|
||||||
locale,
|
locale,
|
||||||
@ -251,6 +255,7 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
|
|||||||
results: HistoryVersions.HistoryVersionDataResponse[];
|
results: HistoryVersions.HistoryVersionDataResponse[];
|
||||||
pagination: HistoryVersions.Pagination;
|
pagination: HistoryVersions.Pagination;
|
||||||
}> {
|
}> {
|
||||||
|
const locale = params.locale || (await getDefaultLocale());
|
||||||
const [{ results, pagination }, localeDictionary] = await Promise.all([
|
const [{ results, pagination }, localeDictionary] = await Promise.all([
|
||||||
query.findPage({
|
query.findPage({
|
||||||
...params,
|
...params,
|
||||||
@ -258,7 +263,7 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
|
|||||||
$and: [
|
$and: [
|
||||||
{ contentType: params.contentType },
|
{ contentType: params.contentType },
|
||||||
...(params.documentId ? [{ relatedDocumentId: params.documentId }] : []),
|
...(params.documentId ? [{ relatedDocumentId: params.documentId }] : []),
|
||||||
...(params.locale ? [{ locale: params.locale }] : []),
|
...(locale ? [{ locale }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
populate: ['createdBy'],
|
populate: ['createdBy'],
|
||||||
@ -497,6 +502,7 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
|
|||||||
const data = omit(['id', ...Object.keys(schemaDiff.removed)], dataWithoutMissingRelations);
|
const data = omit(['id', ...Object.keys(schemaDiff.removed)], dataWithoutMissingRelations);
|
||||||
const restoredDocument = await strapi.documents(version.contentType).update({
|
const restoredDocument = await strapi.documents(version.contentType).update({
|
||||||
documentId: version.relatedDocumentId,
|
documentId: version.relatedDocumentId,
|
||||||
|
locale: version.locale,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,6 @@ export declare namespace RestoreHistoryVersion {
|
|||||||
export interface Request {
|
export interface Request {
|
||||||
params: {
|
params: {
|
||||||
versionId: Data.ID;
|
versionId: Data.ID;
|
||||||
documentId: Data.ID;
|
|
||||||
contentType: UID.ContentType;
|
contentType: UID.ContentType;
|
||||||
};
|
};
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
@ -0,0 +1,442 @@
|
|||||||
|
import { createStrapiInstance } from 'api-tests/strapi';
|
||||||
|
import { createAuthRequest } from 'api-tests/request';
|
||||||
|
import { createUtils, describeOnCondition } from 'api-tests/utils';
|
||||||
|
import { createTestBuilder } from 'api-tests/builder';
|
||||||
|
|
||||||
|
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||||
|
|
||||||
|
const collectionTypeUid = 'api::product.product';
|
||||||
|
const collectionTypeModel = {
|
||||||
|
draftAndPublish: true,
|
||||||
|
singularName: 'product',
|
||||||
|
pluralName: 'products',
|
||||||
|
displayName: 'Product',
|
||||||
|
kind: 'collectionType',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleTypeUid = 'api::homepage.homepage';
|
||||||
|
const singleTypeModel = {
|
||||||
|
draftAndPublish: true,
|
||||||
|
singularName: 'homepage',
|
||||||
|
pluralName: 'homepages',
|
||||||
|
displayName: 'Homepage',
|
||||||
|
kind: 'singleType',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: 'string',
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CreateEntryArgs {
|
||||||
|
uid: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
isCollectionType?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateEntryArgs extends CreateEntryArgs {
|
||||||
|
documentId?: string;
|
||||||
|
locale?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describeOnCondition(edition === 'EE')('History API', () => {
|
||||||
|
const builder = createTestBuilder();
|
||||||
|
let strapi;
|
||||||
|
let rq;
|
||||||
|
let collectionTypeDocumentId;
|
||||||
|
let singleTypeDocumentId;
|
||||||
|
|
||||||
|
const createEntry = async ({ uid, data, isCollectionType = true }: CreateEntryArgs) => {
|
||||||
|
const type = isCollectionType ? 'collection-types' : 'single-types';
|
||||||
|
|
||||||
|
const { body } = await rq({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/content-manager/${type}/${uid}`,
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEntry = async ({ uid, documentId, data, locale }: UpdateEntryArgs) => {
|
||||||
|
const type = documentId ? 'collection-types' : 'single-types';
|
||||||
|
const params = documentId ? `${type}/${uid}/${documentId}` : `${type}/${uid}`;
|
||||||
|
|
||||||
|
const { body } = await rq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/${params}`,
|
||||||
|
body: data,
|
||||||
|
qs: { locale },
|
||||||
|
});
|
||||||
|
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUserAndReq = async (
|
||||||
|
userName: string,
|
||||||
|
permissions: { action: string; subject: string }[]
|
||||||
|
) => {
|
||||||
|
const utils = createUtils(strapi);
|
||||||
|
const role = await utils.createRole({
|
||||||
|
name: `role-${userName}`,
|
||||||
|
description: `Role with restricted permissions for ${userName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rolePermissions = await utils.assignPermissionsToRole(role.id, permissions);
|
||||||
|
Object.assign(role, { permissions: rolePermissions });
|
||||||
|
|
||||||
|
const user = await utils.createUser({
|
||||||
|
firstname: userName,
|
||||||
|
lastname: 'User',
|
||||||
|
email: `${userName}.user@strapi.io`,
|
||||||
|
roles: [role.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const rq = await createAuthRequest({ strapi, userInfo: user });
|
||||||
|
|
||||||
|
return rq;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await builder.addContentTypes([collectionTypeModel, singleTypeModel]).build();
|
||||||
|
|
||||||
|
strapi = await createStrapiInstance();
|
||||||
|
rq = await createAuthRequest({ strapi });
|
||||||
|
|
||||||
|
// Create another locale
|
||||||
|
const localeService = strapi.plugin('i18n').service('locales');
|
||||||
|
await localeService.create({ code: 'fr', name: 'French' });
|
||||||
|
|
||||||
|
// Create a collection type to create an initial history version
|
||||||
|
const collectionType = await createEntry({
|
||||||
|
uid: collectionTypeUid,
|
||||||
|
data: {
|
||||||
|
name: 'Product 1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the single type to create an initial history version
|
||||||
|
const singleType = await updateEntry({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
data: {
|
||||||
|
title: 'Welcome',
|
||||||
|
},
|
||||||
|
isCollectionType: false,
|
||||||
|
});
|
||||||
|
// Set the documentIds to test
|
||||||
|
collectionTypeDocumentId = collectionType.data.documentId;
|
||||||
|
singleTypeDocumentId = singleType.data.documentId;
|
||||||
|
|
||||||
|
// Update to create history versions for entries in different locales
|
||||||
|
await Promise.all([
|
||||||
|
updateEntry({
|
||||||
|
documentId: collectionTypeDocumentId,
|
||||||
|
uid: collectionTypeUid,
|
||||||
|
data: {
|
||||||
|
description: 'Hello',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
updateEntry({
|
||||||
|
documentId: collectionTypeDocumentId,
|
||||||
|
uid: collectionTypeUid,
|
||||||
|
locale: 'fr',
|
||||||
|
data: {
|
||||||
|
name: 'Produit 1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
updateEntry({
|
||||||
|
documentId: collectionTypeDocumentId,
|
||||||
|
uid: collectionTypeUid,
|
||||||
|
locale: 'fr',
|
||||||
|
data: {
|
||||||
|
description: 'Coucou',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
updateEntry({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
data: {
|
||||||
|
description: 'Wow, amazing!',
|
||||||
|
},
|
||||||
|
isCollectionType: false,
|
||||||
|
}),
|
||||||
|
updateEntry({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
data: {
|
||||||
|
title: 'Bienvenue',
|
||||||
|
},
|
||||||
|
isCollectionType: false,
|
||||||
|
locale: 'fr',
|
||||||
|
}),
|
||||||
|
updateEntry({
|
||||||
|
uid: singleTypeUid,
|
||||||
|
data: {
|
||||||
|
description: 'Super',
|
||||||
|
},
|
||||||
|
isCollectionType: false,
|
||||||
|
locale: 'fr',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await strapi.destroy();
|
||||||
|
await builder.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Find many history versions', () => {
|
||||||
|
test('A collection type throws with invalid query params', async () => {
|
||||||
|
const noDocumentId = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${collectionTypeUid}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const noContentTypeUid = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?documentId=${collectionTypeDocumentId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(noDocumentId.statusCode).toBe(403);
|
||||||
|
expect(noContentTypeUid.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A single type throws with invalid query params', async () => {
|
||||||
|
const singleTypeNoContentTypeUid = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(singleTypeNoContentTypeUid.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws without read permissions', async () => {
|
||||||
|
const restrictedRq = await createUserAndReq('restricted', []);
|
||||||
|
const res = await restrictedRq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${collectionTypeUid}&documentId=${collectionTypeDocumentId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A collection type finds many versions in the default locale', async () => {
|
||||||
|
const collectionType = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${collectionTypeUid}&documentId=${collectionTypeDocumentId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(collectionType.statusCode).toBe(200);
|
||||||
|
expect(collectionType.body.data).toHaveLength(2);
|
||||||
|
expect(collectionType.body.data[0].relatedDocumentId).toBe(collectionTypeDocumentId);
|
||||||
|
expect(collectionType.body.data[1].relatedDocumentId).toBe(collectionTypeDocumentId);
|
||||||
|
expect(collectionType.body.data[0].locale.code).toBe('en');
|
||||||
|
expect(collectionType.body.data[1].locale.code).toBe('en');
|
||||||
|
expect(collectionType.body.meta.pagination).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
pageCount: 1,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A collection type finds many versions in the provided locale', async () => {
|
||||||
|
const collectionType = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${collectionTypeUid}&documentId=${collectionTypeDocumentId}&locale=fr`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(collectionType.statusCode).toBe(200);
|
||||||
|
expect(collectionType.body.data).toHaveLength(2);
|
||||||
|
expect(collectionType.body.data[0].relatedDocumentId).toBe(collectionTypeDocumentId);
|
||||||
|
expect(collectionType.body.data[1].relatedDocumentId).toBe(collectionTypeDocumentId);
|
||||||
|
expect(collectionType.body.data[0].locale.code).toBe('fr');
|
||||||
|
expect(collectionType.body.data[1].locale.code).toBe('fr');
|
||||||
|
expect(collectionType.body.meta.pagination).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
pageCount: 1,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A single type finds many versions in the default locale', async () => {
|
||||||
|
const singleType = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${singleTypeUid}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(singleType.statusCode).toBe(200);
|
||||||
|
expect(singleType.body.data).toHaveLength(2);
|
||||||
|
expect(singleType.body.data[0].relatedDocumentId).toBe(singleTypeDocumentId);
|
||||||
|
expect(singleType.body.data[1].relatedDocumentId).toBe(singleTypeDocumentId);
|
||||||
|
expect(singleType.body.data[0].locale.code).toBe('en');
|
||||||
|
expect(singleType.body.data[1].locale.code).toBe('en');
|
||||||
|
expect(singleType.body.meta.pagination).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
pageCount: 1,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A single type finds many versions in the provided locale', async () => {
|
||||||
|
const singleType = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${singleTypeUid}&locale=fr`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(singleType.statusCode).toBe(200);
|
||||||
|
expect(singleType.body.data).toHaveLength(2);
|
||||||
|
expect(singleType.body.data[0].relatedDocumentId).toBe(singleTypeDocumentId);
|
||||||
|
expect(singleType.body.data[1].relatedDocumentId).toBe(singleTypeDocumentId);
|
||||||
|
expect(singleType.body.data[0].locale.code).toBe('fr');
|
||||||
|
expect(singleType.body.data[1].locale.code).toBe('fr');
|
||||||
|
expect(singleType.body.meta.pagination).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
pageCount: 1,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Applies pagination params', async () => {
|
||||||
|
const collectionType = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/content-manager/history-versions/?contentType=${collectionTypeUid}&documentId=${collectionTypeDocumentId}&page=1&pageSize=1`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(collectionType.body.data).toHaveLength(1);
|
||||||
|
expect(collectionType.body.meta.pagination).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
pageCount: 2,
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Restore a history version', () => {
|
||||||
|
test('Throws with invalid body', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/history-versions/1/restore`,
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
status: 400,
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: 'contentType is required',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Throws without update permissions', async () => {
|
||||||
|
const restrictedRq = await createUserAndReq('read', [
|
||||||
|
{ action: 'plugin::content-manager.explorer.read', subject: collectionTypeUid },
|
||||||
|
]);
|
||||||
|
const res = await restrictedRq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/history-versions/1/restore`,
|
||||||
|
body: {
|
||||||
|
contentType: collectionTypeUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
expect(res.body).toMatchObject({
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
status: 403,
|
||||||
|
name: 'ForbiddenError',
|
||||||
|
message: 'Forbidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Restores a history version in the default locale', async () => {
|
||||||
|
const currentDocument = await strapi
|
||||||
|
.documents(collectionTypeUid)
|
||||||
|
.findOne({ documentId: collectionTypeDocumentId });
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/history-versions/1/restore`,
|
||||||
|
body: {
|
||||||
|
contentType: collectionTypeUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredDocument = await strapi
|
||||||
|
.documents(collectionTypeUid)
|
||||||
|
.findOne({ documentId: collectionTypeDocumentId });
|
||||||
|
|
||||||
|
expect(currentDocument.description).toBe('Hello');
|
||||||
|
expect(restoredDocument.description).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Restores a history version in the provided locale', async () => {
|
||||||
|
const currentDocument = await strapi
|
||||||
|
.documents(collectionTypeUid)
|
||||||
|
.findOne({ documentId: collectionTypeDocumentId, locale: 'fr' });
|
||||||
|
|
||||||
|
await rq({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/content-manager/history-versions/4/restore`,
|
||||||
|
body: {
|
||||||
|
contentType: collectionTypeUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredDocument = await strapi
|
||||||
|
.documents(collectionTypeUid)
|
||||||
|
.findOne({ documentId: collectionTypeDocumentId, locale: 'fr' });
|
||||||
|
|
||||||
|
expect(currentDocument.description).toBe('Coucou');
|
||||||
|
expect(restoredDocument.description).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user