diff --git a/packages/strapi-database/tests/migration-draft-publish.test.e2e.js b/packages/strapi-database/tests/migration-draft-publish.test.e2e.js index 539a4d2cd7..2252047caa 100644 --- a/packages/strapi-database/tests/migration-draft-publish.test.e2e.js +++ b/packages/strapi-database/tests/migration-draft-publish.test.e2e.js @@ -55,7 +55,9 @@ describe('Migration - draft and publish', () => { ['with table modifications', { town: { type: 'string' } }, { color: { type: 'string' } }], ])('%p', (testName, tableModification1, tableModification2) => { beforeAll(async () => { - builder = await createTestBuilder() + builder = createTestBuilder(); + + await builder .addContentType(dogModel) .addFixtures(dogModel.name, dogs) .build(); diff --git a/test/helpers/builder/action-registry.js b/test/helpers/builder/action-registry.js new file mode 100644 index 0000000000..af1c1d3497 --- /dev/null +++ b/test/helpers/builder/action-registry.js @@ -0,0 +1,98 @@ +'use strict'; + +const { concat, merge, isFunction, map } = require('lodash/fp'); +const modelsUtils = require('../models'); + +const stringifyDates = object => + JSON.parse( + JSON.stringify(object, (key, value) => { + if (this[key] instanceof Date) { + return this[key].toUTCString(); + } + return value; + }) + ); + +const formatFixtures = map(stringifyDates); + +module.exports = { + ct: { + create: contentType => { + let createdModel; + + return { + async build(state) { + createdModel = await modelsUtils.createContentType(contentType); + return { ...state, models: [...state.models, createdModel] }; + }, + cleanup: () => modelsUtils.deleteContentType(createdModel.modelName), + }; + }, + + createBatch: contentTypes => { + let createdModels = []; + + return { + async build(state) { + createdModels = await modelsUtils.createContentTypes(contentTypes); + return { ...state, models: concat(state.models, createdModels) }; + }, + async cleanup() { + for (const model of createdModels) { + await modelsUtils.deleteContentType(model.modelName); + } + }, + }; + }, + + createMany: contentTypes => { + const createdModels = []; + + return { + async build(state) { + for (const contentType of contentTypes) { + createdModels.push(await modelsUtils.createContentType(contentType)); + } + + return { ...state, models: concat(state.models, createdModels) }; + }, + async cleanup() { + for (const model of createdModels) { + await modelsUtils.deleteContentType(model.modelName); + } + }, + }; + }, + }, + comp: { + create: component => { + let createdModel; + + return { + async build(state) { + createdModel = await modelsUtils.createComponent(component); + return { ...state, models: [...state.models, createdModel] }; + }, + cleanup: () => modelsUtils.deleteComponent(createdModel.uid), + }; + }, + }, + fixtures: { + create(modelName, entries, getFixtures) { + let createdEntries = []; + + return { + async build(state) { + createdEntries = formatFixtures( + await modelsUtils.createFixturesFor( + modelName, + isFunction(entries) ? entries(getFixtures()) : entries + ) + ); + return { ...state, fixtures: merge(state.fixtures, { [modelName]: createdEntries }) }; + }, + cleanup: () => modelsUtils.deleteFixturesFor(modelName, createdEntries), + }; + }, + }, +}; diff --git a/test/helpers/builder/action.js b/test/helpers/builder/action.js deleted file mode 100644 index 6853e0c739..0000000000 --- a/test/helpers/builder/action.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -const { prop, isArray, curry, isNil, has, isFunction } = require('lodash/fp'); -const modelsUtils = require('../models'); - -const bindEvent = ({ event, fn, batch = false, metadata = {} } = {}) => { - Object.assign(fn, { - emitInfos: { - event, - batch, - metadata, - }, - }); - - return fn; -}; - -const registry = { - ct: { - create: bindEvent({ - event: 'model.created', - metadata: { type: 'ct' }, - fn: modelsUtils.createContentType, - }), - createBatch: bindEvent({ - event: 'model.created', - metadata: { type: 'ct' }, - fn: modelsUtils.createContentTypes, - batch: true, - }), - createMany: bindEvent({ - event: 'model.created', - metadata: { type: 'ct' }, - fn: async cts => { - const createdModels = []; - - for (const ct of cts) { - createdModels.push(await modelsUtils.createContentType(ct)); - } - - return createdModels; - }, - }), - }, - comp: { - create: bindEvent({ - event: 'model.created', - metadata: { type: 'comp' }, - fn: modelsUtils.createComponent, - }), - }, - fixture: { - create: bindEvent({ - event: 'fixture.created', - batch: true, - fn: async (model, entries, getFixtures) => { - const entriesToCreate = isFunction(entries) ? entries(getFixtures()) : entries; - const result = await modelsUtils.createFixturesFor(model, entriesToCreate); - return { entries: result, model }; - }, - }), - }, -}; - -const getActionByCode = code => prop(code, registry); - -const createAction = (code, ...params) => { - const _state = { - emitter: null, - params, - code, - fn: getActionByCode(code), - }; - - return { - get params() { - return _state.params; - }, - - get code() { - return _state.code; - }, - - get fn() { - return _state.fn; - }, - - setEmitter(emitter) { - _state.emitter = emitter; - return this; - }, - - async execute() { - const { fn, params, emitter } = _state; - - const res = await fn(...params); - - if (isNil(emitter) || !has('emitInfos', fn)) { - return res; - } - - const { event, batch, metadata } = fn.emitInfos; - - const emitEvent = curry(emitter.emit)(event); - const bindMetadata = result => ({ result, metadata }); - - if (isArray(res) && !batch) { - res.map(bindMetadata).forEach(emitEvent); - } else { - emitEvent(bindMetadata(res)); - } - - return res; - }, - }; -}; - -module.exports = { - createAction, -}; diff --git a/test/helpers/builder/event.js b/test/helpers/builder/event.js deleted file mode 100644 index 8b91d40c02..0000000000 --- a/test/helpers/builder/event.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -// eslint-disable-next-line node/no-extraneous-require -const { contains, reduce, over } = require('lodash/fp'); - -const createEvent = (eventName, data) => ({ name: eventName, data, emittedAt: new Date() }); - -const createEventsManager = ({ allowedEvents = [] }) => { - const toEventsMap = reduce((acc, eventName) => ({ ...acc, [eventName]: [] }), {}); - - const _state = { - events: toEventsMap(allowedEvents), - callbacks: toEventsMap(allowedEvents), - allowedEvents, - }; - - const pushEvent = event => _state.events[event.name].push(event.data); - const notify = ({ name, data }) => over(_state.callbacks[name])(data); - const validateEventName = eventName => { - if (!contains(eventName, _state.allowedEvents)) { - throw new Error(`"${eventName}" is not a valid event name.`); - } - } - - return { - get eventsMap() { - return _state.events; - }, - - get allowedEvents() { - return _state.allowedEvents; - }, - - getEventsByName(name) { - validateEventName(name); - - return _state.events[name]; - }, - - register(eventName, callback) { - validateEventName(eventName); - - _state.callbacks[eventName].push(callback); - - return this; - }, - - unregister(eventName, callback) { - validateEventName(eventName); - - _state.callbacks[eventName] = _state.callbacks[eventName].filter(cb => cb !== callback); - - return this; - }, - - emit(eventName, data) { - validateEventName(eventName); - - const event = createEvent(eventName, data); - const executeOperations = over([pushEvent, notify]); - - executeOperations(event); - - return this; - } - } -} - -module.exports = { - createEvent, - createEventsManager, -}; diff --git a/test/helpers/builder/index.js b/test/helpers/builder/index.js index 013fb64d4f..2bad2962ea 100644 --- a/test/helpers/builder/index.js +++ b/test/helpers/builder/index.js @@ -1,108 +1,30 @@ 'use strict'; -// eslint-disable-next-line node/no-extraneous-require +const { omit, get } = require('lodash/fp'); const _ = require('lodash'); -// eslint-disable-next-line node/no-extraneous-require -const { map, prop } = require('lodash/fp'); const modelsUtils = require('../models'); const { sanitizeEntity } = require('../../../packages/strapi-utils'); -const { createAction } = require('./action'); -const { createEventsManager } = require('./event'); +const actionRegistry = require('./action-registry'); -const events = { - MODEL_CREATED: 'model.created', - MODEL_DELETED: 'model.deleted', - FIXTURES_CREATED: 'fixture.created', -}; - -const stringifyDates = object => - JSON.parse( - JSON.stringify(object, (key, value) => { - if (this[key] instanceof Date) { - return this[key].toUTCString(); - } - return value; - }) - ); - -const formatFixtures = map(stringifyDates); - -/** - * Create a test builder from the given args. - * - * A builder allows to create resources (content-types, components, fixtures) that can be cleaned-up easily. - * - * @param options The builder's constructor options - * @param options.initialState {State} The builder's initial state - * - * @example ``` - * // Setup the builder - * const builder = createTestBuilder() - * .addContentType(articleModel) - * .addComponent(myComponent); - * - * // Run the build operations - * await builder.build(); - * - * // ... - * - * // Cleanup created resources - * await builder.cleanup(); - * ``` - */ const createTestBuilder = (options = {}) => { const { initialState } = options; + const omitActions = omit('actions'); + const getDefaultState = () => ({ actions: [], models: [], fixtures: {}, ...initialState }); + + const state = getDefaultState(); const addAction = (code, ...params) => { - const action = createAction(code, ...params).setEmitter(_eventsManager); - _state.actions.push(action); - return action; + const action = get(code, actionRegistry); + state.actions.push(action(...params)); }; - const getDefaultState = () => ({ - actions: [], - deleted: [], - created: [], - fixtures: {}, - ...initialState, - }); - - const getModelsMap = (type = null) => - _.difference(_state.created, _state.deleted) - // Keep the models with the wanted type (all, content-types, or components) - .filter(event => _.isNull(type) || _.get(event, 'metadata.type') === type) - // Flatten the data property to obtain an array of model - .flatMap(_.property('result')) - // Transform the array into a map where the key is the model's name - .reduce((acc, model) => _.merge(acc, { [model.modelName]: model }), {}); - - const _state = getDefaultState(); - - const _eventsManager = createEventsManager({ allowedEvents: Object.values(events) }); - - _eventsManager - .register(events.MODEL_CREATED, event => _state.created.push(event)) - .register(events.MODEL_DELETED, event => _state.deleted.push(event)) - .register(events.FIXTURES_CREATED, ({ result }) => { - const existingFixtures = _.get(_state.fixtures, result.model, []); - _state.fixtures[result.model] = existingFixtures.concat(formatFixtures(result.entries)); - }); - return { get models() { - return getModelsMap(); - }, - - get contentTypes() { - return getModelsMap('ct'); - }, - - get components() { - return getModelsMap('comp'); + return state.models; }, get fixtures() { - return _state.fixtures; + return state.fixtures; }, sanitizedFixtures(strapi) { @@ -135,73 +57,32 @@ const createTestBuilder = (options = {}) => { return this; }, - addFixtures(model, entries, { onCreated = null } = {}) { - addAction('fixture.create', model, entries, () => this.fixtures); - - if (_.isFunction(onCreated)) { - _eventsManager.register(events.FIXTURES_CREATED, e => { - const { result } = e; - - // Filters fixture.created events triggered for other models - if (result.model === model) { - onCreated(formatFixtures(result.entries)); - } - }); - } - + addFixtures(model, entries) { + addAction('fixtures.create', model, entries, () => this.fixtures); return this; }, - /** - * Execute every registered step - */ async build() { - for (const action of _state.actions) { - await action.execute(); + for (const action of state.actions) { + const newState = await action.build(omitActions(state)); + Object.assign(state, newState); } - return this; }, - /** - * Cleanup and delete every model (content-types / components) created by the builder - */ - async cleanup() { - // The first model to be created should be the last one deleted - const createdEvents = _state.created.reverse(); + async cleanup(options = {}) { + const { enableTestDataAutoCleanup = true } = options; - for (const event of createdEvents) { - const { - result, - metadata: { type }, - } = event; - - // Helpers for the given model - const isBatchOperation = _.isArray(result); - const modelType = type === 'ct' ? 'ContentType' : 'Component'; - const pluralSuffix = isBatchOperation ? 's' : ''; - - // Pluralized & Type related methods name - const cleanupMethod = `cleanupModel${pluralSuffix}`; - const deleteMethod = `delete${modelType}${pluralSuffix}`; - - // Format params - const uidAttribute = type === 'ct' ? 'modelName' : 'uid'; - const params = isBatchOperation - ? result.map(prop(uidAttribute)) - : prop(uidAttribute, result); - - // Execute cleanup & delete operations - await modelsUtils[cleanupMethod](params); - await modelsUtils[deleteMethod](params); - - // Notify the builder that the created model has been deleted - _eventsManager.emit(events.MODEL_DELETED, event); + if (enableTestDataAutoCleanup) { + for (const model of state.models.reverse()) { + await modelsUtils.cleanupModel(model.uid || model.modelName); + } + } + + for (const action of state.actions.reverse()) { + await action.cleanup(); } - return this; }, }; }; -module.exports = { - createTestBuilder, -}; +module.exports = { createTestBuilder }; diff --git a/test/helpers/models.js b/test/helpers/models.js index bca788c297..8d8fb322cc 100644 --- a/test/helpers/models.js +++ b/test/helpers/models.js @@ -1,7 +1,6 @@ 'use strict'; -// eslint-disable-next-line node/no-extraneous-require -const { isFunction, isNil } = require('lodash/fp'); +const { isFunction, isNil, prop } = require('lodash/fp'); const { createStrapiInstance } = require('./strapi'); const createHelpers = async ({ strapi: strapiInstance = null, ...options } = {}) => { @@ -172,6 +171,14 @@ async function createFixturesFor(model, entries, { strapi: strapiIst } = {}) { return results; } +async function deleteFixturesFor(model, entries, { strapi: strapiIst } = {}) { + const { strapi, cleanup } = await createHelpers({ strapi: strapiIst }); + + await strapi.query(model).delete({ id_in: entries.map(prop('id')) }); + + await cleanup(); +} + async function modifyContentType(data, { strapi } = {}) { const { contentTypeService, cleanup } = await createHelpers({ strapi }); @@ -223,6 +230,7 @@ module.exports = { // Fixtures createFixtures, createFixturesFor, + deleteFixturesFor, // Update Content-Types modifyContentType, // Misc