Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(history): refactor restore and add tests for relations #20179

Merged
merged 10 commits into from May 6, 2024
4 changes: 2 additions & 2 deletions packages/core/content-manager/server/src/history/index.ts
Expand Up @@ -17,10 +17,10 @@ const getFeature = (): Partial<Plugin.LoadedPlugin> => {
},
bootstrap({ strapi }) {
// Start recording history and saving history versions
getService(strapi, 'history').bootstrap();
getService(strapi, 'lifecycles').bootstrap();
},
destroy({ strapi }) {
getService(strapi, 'history').destroy();
getService(strapi, 'lifecycles').destroy();
},
controllers,
services,
Expand Down
@@ -1,16 +1,11 @@
import type { UID } from '@strapi/types';
import { scheduleJob } from 'node-schedule';
import { HISTORY_VERSION_UID } from '../../constants';
import { createHistoryService } from '../history';

const createMock = jest.fn();
const userId = 'user-id';
const fakeDate = new Date('1970-01-01T00:00:00.000Z');

jest.mock('node-schedule', () => ({
scheduleJob: jest.fn(),
}));

const mockGetRequestContext = jest.fn(() => {
return {
state: {
Expand Down Expand Up @@ -113,8 +108,6 @@ const mockStrapi = {
}
},
};
// @ts-expect-error - ignore
mockStrapi.documents.use = jest.fn();

// @ts-expect-error - we're not mocking the full Strapi object
const historyService = createHistoryService({ strapi: mockStrapi });
Expand All @@ -124,122 +117,6 @@ describe('history-version service', () => {
jest.useRealTimers();
});

it('inits service only once', () => {
historyService.bootstrap();
historyService.bootstrap();
// @ts-expect-error - ignore
expect(mockStrapi.documents.use).toHaveBeenCalledTimes(1);
});

it('saves relevant document actions in history', async () => {
const context = {
action: 'create',
contentType: {
uid: 'api::article.article',
},
params: {
locale: 'fr',
},
};

const next = jest.fn((context) => ({ ...context, documentId: 'document-id' }));
await historyService.bootstrap();
// @ts-expect-error - ignore
const historyMiddlewareFunction = mockStrapi.documents.use.mock.calls[0][0];

// Check that we don't break the middleware chain
await historyMiddlewareFunction(context, next);
expect(next).toHaveBeenCalled();

// Ensure we're only storing the data we need in the database
expect(mockFindOne).toHaveBeenLastCalledWith({
documentId: 'document-id',
locale: 'fr',
populate: {
component: {
populate: {
relation: {
fields: ['documentId', 'locale', 'publishedAt'],
},
medias: {
fields: ['id'],
},
},
},
relation: {
fields: ['documentId', 'locale', 'publishedAt'],
},
media: {
fields: ['id'],
},
},
});

// Create and update actions should be saved in history
const createPayload = createMock.mock.calls.at(-1)[0].data;
expect(createPayload.schema).toEqual({
title: {
type: 'string',
},
relation: {
type: 'relation',
target: 'api::category.category',
},
component: {
type: 'component',
component: 'some.component',
},
media: {
type: 'media',
},
});
expect(createPayload.componentsSchemas).toEqual({
'some.component': {
title: {
type: 'string',
},
relation: {
type: 'relation',
target: 'api::restaurant.restaurant',
},
medias: {
type: 'media',
multiple: true,
},
},
});
context.action = 'update';
await historyMiddlewareFunction(context, next);
expect(createMock).toHaveBeenCalledTimes(2);

// Publish and unpublish actions should be saved in history
createMock.mockClear();
await historyMiddlewareFunction(context, next);
context.action = 'unpublish';
await historyMiddlewareFunction(context, next);
expect(createMock).toHaveBeenCalledTimes(2);

// Other actions should be ignored
createMock.mockClear();
context.action = 'findOne';
await historyMiddlewareFunction(context, next);
context.action = 'delete';
await historyMiddlewareFunction(context, next);
expect(createMock).toHaveBeenCalledTimes(0);

// Non-api content types should be ignored
createMock.mockClear();
context.contentType.uid = 'plugin::upload.file';
context.action = 'create';
await historyMiddlewareFunction(context, next);
expect(createMock).toHaveBeenCalledTimes(0);

// Don't break middleware chain even if we don't save the action in history
next.mockClear();
await historyMiddlewareFunction(context, next);
expect(next).toHaveBeenCalled();
});

it('creates a history version with the author', async () => {
jest.useFakeTimers().setSystemTime(fakeDate);

Expand All @@ -259,6 +136,7 @@ describe('history-version service', () => {
status: 'draft' as const,
};

// @ts-expect-error - ignore
markkaylor marked this conversation as resolved.
Show resolved Hide resolved
await historyService.createVersion(historyVersionData);
expect(createMock).toHaveBeenCalledWith({
data: {
Expand Down Expand Up @@ -290,6 +168,7 @@ describe('history-version service', () => {

mockGetRequestContext.mockReturnValueOnce(null as any);

// @ts-expect-error - ignore
await historyService.createVersion(historyVersionData);
expect(createMock).toHaveBeenCalledWith({
data: {
Expand All @@ -299,16 +178,4 @@ describe('history-version service', () => {
},
});
});

it('should create a cron job that runs once a day', async () => {
// @ts-expect-error - this is a mock
const mockScheduleJob = scheduleJob.mockImplementationOnce(
jest.fn((rule, callback) => callback())
);

await historyService.bootstrap();

expect(mockScheduleJob).toHaveBeenCalledTimes(1);
expect(mockScheduleJob).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
});
});
@@ -0,0 +1,102 @@
import type { UID } from '@strapi/types';
import { scheduleJob } from 'node-schedule';
import { HISTORY_VERSION_UID } from '../../constants';
import { createLifecyclesService } from '../lifecycles';

jest.mock('node-schedule', () => ({
scheduleJob: jest.fn(),
}));

const mockGetRequestContext = jest.fn(() => {
return {
state: {
user: {
id: '123',
},
},
request: {
url: '/content-manager/test',
},
};
});

const mockStrapi = {
service: jest.fn(),
plugins: {
'content-manager': {
service: jest.fn(() => ({
getMetadata: jest.fn().mockResolvedValue([]),
getStatus: jest.fn(),
})),
},
i18n: {
service: jest.fn(() => ({
getDefaultLocale: jest.fn().mockReturnValue('en'),
})),
},
},
// @ts-expect-error - Ignore
plugin: (plugin: string) => mockStrapi.plugins[plugin],
db: {
query(uid: UID.ContentType) {
if (uid === HISTORY_VERSION_UID) {
return {
create: jest.fn(),
};
}
},
transaction(cb: any) {
const opt = {
onCommit(func: any) {
return func();
},
};
return cb(opt);
},
},
ee: {
features: {
isEnabled: jest.fn().mockReturnValue(false),
get: jest.fn(),
},
},
documents: jest.fn(() => ({
findOne: jest.fn(),
})),
requestContext: {
get: mockGetRequestContext,
},
config: {
get: () => undefined,
},
};
// @ts-expect-error - ignore
mockStrapi.documents.use = jest.fn();

// @ts-expect-error - we're not mocking the full Strapi object
const lifecyclesService = createLifecyclesService({ strapi: mockStrapi });

describe('history lifecycles service', () => {
afterEach(() => {
jest.useRealTimers();
});

it('inits service only once', () => {
lifecyclesService.bootstrap();
lifecyclesService.bootstrap();
// @ts-expect-error - ignore
expect(mockStrapi.documents.use).toHaveBeenCalledTimes(1);
});

it('should create a cron job that runs once a day', async () => {
// @ts-expect-error - this is a mock
const mockScheduleJob = scheduleJob.mockImplementationOnce(
jest.fn((rule, callback) => callback())
);

await lifecyclesService.bootstrap();

expect(mockScheduleJob).toHaveBeenCalledTimes(1);
expect(mockScheduleJob).toHaveBeenCalledWith('0 0 * * *', expect.any(Function));
});
});
@@ -1,7 +1,16 @@
import { getSchemaAttributesDiff } from '../utils';
import { createServiceUtils } from '../utils';

describe('history-version service utils', () => {
const baseStrapiMock = {
plugin: jest.fn(() => {}),
};

describe('History utils', () => {
describe('getSchemaAttributesDiff', () => {
const { getSchemaAttributesDiff } = createServiceUtils({
// @ts-expect-error ignore
strapi: baseStrapiMock,
});

it('should return a diff', () => {
const versionSchema = {
title: {
Expand Down