diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 25399175d9..b284692abd 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,11 +1,9 @@ -version: '3' - services: postgres: image: postgres restart: always volumes: - - pgdata_test:/var/lib/postgresql/data + - pgdata_test:/var/lib/postgresql environment: POSTGRES_USER: strapi POSTGRES_PASSWORD: strapi diff --git a/tests/api/core/strapi/document-service/update.test.api.ts b/tests/api/core/strapi/document-service/update.test.api.ts index 671bd8e4ec..35e253729a 100644 --- a/tests/api/core/strapi/document-service/update.test.api.ts +++ b/tests/api/core/strapi/document-service/update.test.api.ts @@ -3,7 +3,7 @@ import type { Core, Modules } from '@strapi/types'; import { omit } from 'lodash/fp'; import { createTestSetup, destroyTestSetup } from '../../../utils/builder-helper'; -import { testInTransaction } from '../../../utils/index'; +import { setupDatabaseReset } from '../../../utils/index'; import resources from './resources/index'; import { ARTICLE_UID, findArticleDb } from './utils'; @@ -25,8 +25,10 @@ describe('Document Service', () => { await destroyTestSetup(testUtils); }); + setupDatabaseReset(); + describe('Update', () => { - testInTransaction('Can update a draft', async () => { + it('Can update a draft', async () => { const articleDb = await findArticleDb({ title: 'Article1-Draft-EN' }); const data = { @@ -55,7 +57,7 @@ describe('Document Service', () => { }); }); - testInTransaction('Can update a draft article in dutch', async () => { + it('Can update a draft article in dutch', async () => { const articleDb = await findArticleDb({ title: 'Article1-Draft-NL' }); const data = { title: 'updated document' }; @@ -78,7 +80,7 @@ describe('Document Service', () => { expect(enLocale).toBeDefined(); }); - testInTransaction('Create a new locale for an existing document', async () => { + it('Create a new locale for an existing document', async () => { const articleDb = await findArticleDb({ title: 'Article1-Draft-EN' }); const newName = 'updated document'; @@ -106,7 +108,7 @@ describe('Document Service', () => { expect(enLocale).toBeDefined(); }); - testInTransaction('Can update a draft and publish it', async () => { + it('Can update a draft and publish it', async () => { const articleDb = await findArticleDb({ title: 'Article1-Draft-EN' }); const article = await updateArticle({ @@ -121,7 +123,7 @@ describe('Document Service', () => { }); }); - testInTransaction('Returns null if document to update does not exist', async () => { + it('Returns null if document to update does not exist', async () => { const article = await updateArticle({ documentId: 'does-not-exist', data: { title: 'updated document' }, @@ -130,50 +132,47 @@ describe('Document Service', () => { expect(article).toBeNull(); }); - testInTransaction( - 'Preserves non-localized fields when updating localized content for new locale', - async () => { - // Covers issue https://github.com/strapi/strapi/issues/21594 + it('Preserves non-localized fields when updating localized content for new locale', async () => { + // Covers issue https://github.com/strapi/strapi/issues/21594 - const MIXED_CONTENT_UID = 'api::mixed-content.mixed-content'; + const MIXED_CONTENT_UID = 'api::mixed-content.mixed-content'; - // Create a document with both localized and non-localized fields - const originalDoc = await strapi.documents(MIXED_CONTENT_UID).create({ - data: { - localizedText: 'Original Text', - sharedText: 'Shared Content', - }, - locale: 'en', - }); - - const updatedDoc = await strapi.documents(MIXED_CONTENT_UID).update({ - documentId: originalDoc.documentId, - locale: 'es', - data: { - localizedText: 'Texto Español', - }, - }); - - expect(updatedDoc).toMatchObject({ - documentId: originalDoc.documentId, - locale: 'es', - localizedText: 'Texto Español', - // Non-localized field should remain unchanged - sharedText: 'Shared Content', - }); - - const originalEnDoc = await strapi.documents(MIXED_CONTENT_UID).findOne({ - documentId: originalDoc.documentId, - locale: 'en', - }); - - expect(originalEnDoc).toMatchObject({ - documentId: originalDoc.documentId, - locale: 'en', + // Create a document with both localized and non-localized fields + const originalDoc = await strapi.documents(MIXED_CONTENT_UID).create({ + data: { localizedText: 'Original Text', sharedText: 'Shared Content', - }); - } - ); + }, + locale: 'en', + }); + + const updatedDoc = await strapi.documents(MIXED_CONTENT_UID).update({ + documentId: originalDoc.documentId, + locale: 'es', + data: { + localizedText: 'Texto Español', + }, + }); + + expect(updatedDoc).toMatchObject({ + documentId: originalDoc.documentId, + locale: 'es', + localizedText: 'Texto Español', + // Non-localized field should remain unchanged + sharedText: 'Shared Content', + }); + + const originalEnDoc = await strapi.documents(MIXED_CONTENT_UID).findOne({ + documentId: originalDoc.documentId, + locale: 'en', + }); + + expect(originalEnDoc).toMatchObject({ + documentId: originalDoc.documentId, + locale: 'en', + localizedText: 'Original Text', + sharedText: 'Shared Content', + }); + }); }); }); diff --git a/tests/api/utils/index.ts b/tests/api/utils/index.ts index 36afc4067b..ab3e810c38 100644 --- a/tests/api/utils/index.ts +++ b/tests/api/utils/index.ts @@ -1,6 +1,89 @@ -// Note: any tests that would cause writes to the db should be wrapped with this method to prevent changes -// Alternatively, we could truncate/insert the tables in afterEach which should be only marginally slower -// TODO: move to utils +/* ------------------------------------------------------------------------------------------------- + * setupDatabaseReset + * -----------------------------------------------------------------------------------------------*/ + +// Store initial database state for reset +let initialTestData = {}; +let isTestDataCaptured = false; +let allTableNames = []; + +/** + * Capture the initial state of all database tables for later restoration + */ +async function captureInitialTestData() { + if (isTestDataCaptured) return; + + initialTestData = {}; + + // Use Strapi's built-in dialect system to get table names + allTableNames = await strapi.db.dialect.schemaInspector.getTables(); + + for (const tableName of allTableNames) { + try { + const data = await strapi.db.connection(tableName).select('*'); + initialTestData[tableName] = data; + } catch (error) { + console.warn(`Could not capture data for table ${tableName}:`, error.message); + initialTestData[tableName] = []; + } + } + + isTestDataCaptured = true; +} + +async function resetTestDatabase() { + // Use Strapi's built-in schema update mechanism to disable constraints (e.g. foreign key constraints) + await strapi.db.dialect.startSchemaUpdate(); + + try { + for (const tableName of allTableNames) { + try { + // Clear the table + await strapi.db.connection(tableName).del(); + + // Restore initial data if any + const initialData = initialTestData[tableName]; + if (initialData && initialData.length > 0) { + await strapi.db.connection(tableName).insert(initialData); + } + } catch (error) { + console.warn(`Could not reset table ${tableName}:`, error.message); + } + } + } finally { + // Always re-enable constraints + await strapi.db.dialect.endSchemaUpdate(); + } +} + +/** + * Setup database reset for a test suite + * Call this in your describe block to automatically reset after each test + * + * NOTE: + * Only use sparingly where needed as the operation is slower than testInTransaction + */ +export function setupDatabaseReset() { + let isDataCaptured = false; + + beforeEach(async () => { + if (!isDataCaptured) { + await captureInitialTestData(); + isDataCaptured = true; + } + }); + + afterEach(async () => { + if (isDataCaptured) { + await resetTestDatabase(); + } + }); +} + +/* ------------------------------------------------------------------------------------------------- + * testInTransaction + * -----------------------------------------------------------------------------------------------*/ + export const wrapInTransaction = (test) => { return async (...args) => { await strapi.db.transaction(async ({ trx, rollback }) => { @@ -10,6 +93,13 @@ export const wrapInTransaction = (test) => { }; }; +/** + * Resets the database by leveragin a transaction rollback + * + * NOTE: + * Alternatively, use setupDatabaseReset() which is slower but avoids errors thrown by asnyc operations + * executed after the test's transaction context has closed. + */ export const testInTransaction = (...args: Parameters) => { if (args.length > 1) { return it(