Skip to content

Commit

Permalink
chore(history): refactor restore and add tests for relations (#20179)
Browse files Browse the repository at this point in the history
  • Loading branch information
markkaylor committed May 6, 2024
1 parent 63d7007 commit e60ec18
Show file tree
Hide file tree
Showing 11 changed files with 818 additions and 571 deletions.
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 Down Expand Up @@ -303,16 +180,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

0 comments on commit e60ec18

Please sign in to comment.