Skip to content

fix: resolve issue when updating or adding content type #998

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
'**/__tests__/**/content-types.test.[jt]s?(x)',
'**/__tests__/**/meilisearch.test.[jt]s?(x)',
'**/__tests__/**/configuration.test.[jt]s?(x)',
'**/__tests__/**/lifecycle.test.[jt]s?(x)',
'**/__tests__/**/configuration-validation.test.[jt]s?(x)',
],
}
10 changes: 10 additions & 0 deletions server/src/__mocks__/strapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ function createStrapiMock({
})

const mockAddIndexedContentType = jest.fn(() => {})
const mockAddListenedContentType = jest.fn(() => {})

const mockPluginService = jest.fn(() => {
return {
getContentTypesUid: () => ['restaurant', 'about'],
getContentTypeUid: ({ contentType }) => contentType,
getCollectionName: ({ contentType }) => contentType,
getCredentials: () => ({
host: 'http://localhost:7700',
Expand All @@ -48,6 +50,10 @@ function createStrapiMock({
subscribeContentType: () => {
return
},
// Add methods for Meilisearch operations
addEntriesToMeilisearch: jest.fn(),
updateEntriesInMeilisearch: jest.fn(),
deleteEntriesFromMeiliSearch: jest.fn(),
}
})

Expand All @@ -64,6 +70,9 @@ function createStrapiMock({
return 1
})
const mockDb = {
lifecycles: {
subscribe: jest.fn()
},
query: jest.fn(() => ({
count: mockFindWithCount,
})),
Expand Down Expand Up @@ -92,6 +101,7 @@ function createStrapiMock({
config: mockConfig,
db: mockDb,
documents: mockDocumentService,

}
return mockStrapi
}
Expand Down
320 changes: 320 additions & 0 deletions server/src/__tests__/lifecycle.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import createLifecycle from '../services/lifecycle/lifecycle.js'
import {MeiliSearch} from '../__mocks__/meilisearch'
import {createStrapiMock} from "../__mocks__/strapi"

global.meiliSearch = MeiliSearch

const strapiMock = createStrapiMock({})
global.strapi = strapiMock

// Setup service mocks to handle lifecycle operations
const meilisearchService = {
addEntriesToMeilisearch: jest.fn().mockReturnValue(Promise.resolve()),
updateEntriesInMeilisearch: jest.fn().mockReturnValue(Promise.resolve()),
deleteEntriesFromMeiliSearch: jest.fn().mockReturnValue(Promise.resolve()),
getContentTypesUid: () => ['restaurant', 'about'],
getContentTypeUid: ({ contentType }) => contentType,
getCollectionName: ({ contentType }) => contentType,
entriesQuery: jest.fn(() => ({}))
}

const storeService = {
addListenedContentType: jest.fn(() => ({}))
}

const contentTypeService = {
getContentTypeUid: ({ contentType }) => contentType,
getEntry: jest.fn()
}

// Create a mock of the plugin service function
const originalPlugin = strapiMock.plugin
strapiMock.plugin = jest.fn((pluginName) => {
if (pluginName === 'meilisearch') {
return {
service: jest.fn((serviceName) => {
if (serviceName === 'store') return storeService
if (serviceName === 'meilisearch') return meilisearchService
if (serviceName === 'contentType') return contentTypeService
return originalPlugin().service()
})
}
}
return originalPlugin(pluginName)
})

describe('Lifecycle Meilisearch integration', () => {
let lifecycleHandler

beforeEach(async () => {
jest.clearAllMocks()
jest.restoreAllMocks()

// Reset all mocks for clean state
meilisearchService.addEntriesToMeilisearch.mockClear().mockReturnValue(Promise.resolve());
meilisearchService.updateEntriesInMeilisearch.mockClear().mockReturnValue(Promise.resolve());
meilisearchService.deleteEntriesFromMeiliSearch.mockClear().mockReturnValue(Promise.resolve());

contentTypeService.getEntries = jest.fn().mockResolvedValue([{ id: '1', title: 'Test' }]);
contentTypeService.numberOfEntries = jest.fn().mockResolvedValue(5);

lifecycleHandler = createLifecycle({ strapi: strapiMock })
})

test('should add entry to Meilisearch on afterCreate', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123', title: 'Test Entry' };

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterCreate({ result });

expect(meilisearchService.addEntriesToMeilisearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entries: [result]
});
expect(storeService.addListenedContentType).toHaveBeenCalledWith({
contentType: contentTypeUid
});
});

test('should handle error during afterCreate', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123', title: 'Test Entry' };
const error = new Error('Connection failed');

// Mock error scenario
meilisearchService.addEntriesToMeilisearch.mockRejectedValueOnce(error);
jest.spyOn(strapiMock.log, 'error');

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterCreate({ result });

expect(strapiMock.log.error).toHaveBeenCalledWith(
`Meilisearch could not add entry with id: ${result.id}: ${error.message}`
);
});

test('should process multiple entries on afterCreateMany', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = {
count: 3,
ids: ['1', '2', '3']
};

const mockEntries = [
{ id: '1', title: 'Entry 1' },
{ id: '2', title: 'Entry 2' },
{ id: '3', title: 'Entry 3' }
];

contentTypeService.getEntries.mockResolvedValueOnce(mockEntries);

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterCreateMany({ result });

expect(contentTypeService.getEntries).toHaveBeenCalledWith({
contentType: contentTypeUid,
start: 0,
limit: 500,
filters: {
id: {
$in: result.ids
}
}
});

expect(meilisearchService.updateEntriesInMeilisearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entries: mockEntries
});
});

test('should handle error during afterCreateMany', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = {
count: 3,
ids: ['1', '2', '3']
};

const mockEntries = [
{ id: '1', title: 'Entry 1' },
{ id: '2', title: 'Entry 2' },
{ id: '3', title: 'Entry 3' }
];

// Setup the mock to return entries but fail on updateEntriesInMeilisearch
contentTypeService.getEntries.mockResolvedValueOnce(mockEntries);
const error = new Error('Batch update failed');
meilisearchService.updateEntriesInMeilisearch.mockRejectedValueOnce(error);

jest.spyOn(strapiMock.log, 'error');

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterCreateMany({ result });

expect(strapiMock.log.error).toHaveBeenCalledWith(
`Meilisearch could not update the entries: ${error.message}`
);
});

test('should update entry in Meilisearch on afterUpdate', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123', title: 'Updated Entry' };

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterUpdate({ result });

expect(meilisearchService.updateEntriesInMeilisearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entries: [result]
});
});

test('should handle error during afterUpdate', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123', title: 'Updated Entry' };
const error = new Error('Update failed');

meilisearchService.updateEntriesInMeilisearch.mockRejectedValueOnce(error);
jest.spyOn(strapiMock.log, 'error');

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterUpdate({ result });

expect(strapiMock.log.error).toHaveBeenCalledWith(
`Meilisearch could not update entry with id: ${result.id}: ${error.message}`
);
});

test('should process multiple entries on afterUpdateMany', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const event = {
params: {
where: { type: 'restaurant' }
}
};

const mockEntries = [
{ id: '1', title: 'Updated 1' },
{ id: '2', title: 'Updated 2' }
];

contentTypeService.getEntries.mockResolvedValueOnce(mockEntries);

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterUpdateMany(event);

expect(contentTypeService.numberOfEntries).toHaveBeenCalledWith({
contentType: contentTypeUid,
filters: event.params.where
});

expect(contentTypeService.getEntries).toHaveBeenCalledWith({
contentType: contentTypeUid,
filters: event.params.where,
start: 0,
limit: 500
});

expect(meilisearchService.updateEntriesInMeilisearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entries: mockEntries
});
});

test('should handle error during afterUpdateMany', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const event = {
params: {
where: { type: 'restaurant' }
}
};

const mockEntries = [
{ id: '1', title: 'Updated 1' },
{ id: '2', title: 'Updated 2' }
];

// Setup mocks for the success path but failure during Meilisearch update
contentTypeService.getEntries.mockResolvedValueOnce(mockEntries);
const error = new Error('Batch update failed');
meilisearchService.updateEntriesInMeilisearch.mockRejectedValueOnce(error);

jest.spyOn(strapiMock.log, 'error');

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterUpdateMany(event);

expect(strapiMock.log.error).toHaveBeenCalledWith(
`Meilisearch could not update the entries: ${error.message}`
);
});

test('should delete entry from Meilisearch on afterDelete', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123' };

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterDelete({ result });

expect(meilisearchService.deleteEntriesFromMeiliSearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entriesId: [result.id]
});
});

test('should handle multiple ids in afterDelete', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123' };
const params = {
where: {
$and: [
{ id: { $in: ['101', '102', '103'] } }
]
}
};

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterDelete({ result, params });

expect(meilisearchService.deleteEntriesFromMeiliSearch).toHaveBeenCalledWith({
contentType: contentTypeUid,
entriesId: ['101', '102', '103']
});
});

test('should handle error during afterDelete', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const result = { id: '123' };
const error = new Error('Delete failed');

meilisearchService.deleteEntriesFromMeiliSearch.mockRejectedValueOnce(error);
jest.spyOn(strapiMock.log, 'error');

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterDelete({ result });

expect(strapiMock.log.error).toHaveBeenCalledWith(
`Meilisearch could not delete entry with id: ${result.id}: ${error.message}`
);
});

test('should call afterDelete from afterDeleteMany', async () => {
const contentTypeUid = 'api::restaurant.restaurant';
const event = { result: { id: '123' } };

await lifecycleHandler.subscribeContentType({ contentType: contentTypeUid });

// Get a reference to the afterDelete handler
const afterDeleteSpy = jest.spyOn(
strapiMock.db.lifecycles.subscribe.mock.calls[0][0],
'afterDelete'
);

// Call afterDeleteMany
await strapiMock.db.lifecycles.subscribe.mock.calls[0][0].afterDeleteMany(event);

// Verify it calls afterDelete with the same event
expect(afterDeleteSpy).toHaveBeenCalledWith(event);
});
})
Loading