support.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import type { AbstractDialect, Options } from '@sequelize/core';
  2. import { QueryTypes, Sequelize } from '@sequelize/core';
  3. import type { AbstractQuery } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query.js';
  4. import type { SqliteDialect } from '@sequelize/sqlite3';
  5. import uniq from 'lodash/uniq';
  6. import fs from 'node:fs';
  7. import pTimeout from 'p-timeout';
  8. import {
  9. createSequelizeInstance,
  10. getSqliteDatabasePath,
  11. getTestDialect,
  12. rand,
  13. resetSequelizeInstance,
  14. sequelize,
  15. setIsIntegrationTestSuite,
  16. } from '../support';
  17. setIsIntegrationTestSuite(true);
  18. // Store local references to `setTimeout` and `clearTimeout` asap, so that we can use them within `p-timeout`,
  19. // avoiding to be affected unintentionally by `sinon.useFakeTimers()` called by the tests themselves.
  20. const { setTimeout, clearTimeout } = global;
  21. const CLEANUP_TIMEOUT = Number.parseInt(process.env.SEQ_TEST_CLEANUP_TIMEOUT ?? '', 10) || 10_000;
  22. const runningQueries = new Set<AbstractQuery>();
  23. before(async () => {
  24. // Sometimes the SYSTOOLSPACE tablespace is not available when running tests on DB2. This creates it.
  25. if (getTestDialect() === 'db2') {
  26. const res = await sequelize.query<{ TBSPACE: string }>(
  27. `SELECT TBSPACE FROM SYSCAT.TABLESPACES WHERE TBSPACE = 'SYSTOOLSPACE'`,
  28. {
  29. type: QueryTypes.SELECT,
  30. },
  31. );
  32. const tableExists = res[0]?.TBSPACE === 'SYSTOOLSPACE';
  33. if (!tableExists) {
  34. // needed by dropSchema function
  35. await sequelize.query(`
  36. CREATE TABLESPACE SYSTOOLSPACE IN IBMCATGROUP
  37. MANAGED BY AUTOMATIC STORAGE USING STOGROUP IBMSTOGROUP
  38. EXTENTSIZE 4;
  39. `);
  40. await sequelize.query(`
  41. CREATE USER TEMPORARY TABLESPACE SYSTOOLSTMPSPACE IN IBMCATGROUP
  42. MANAGED BY AUTOMATIC STORAGE USING STOGROUP IBMSTOGROUP
  43. EXTENTSIZE 4
  44. `);
  45. }
  46. }
  47. sequelize.hooks.addListener('beforeQuery', (options, query) => {
  48. runningQueries.add(query);
  49. });
  50. sequelize.hooks.addListener('afterQuery', (options, query) => {
  51. runningQueries.delete(query);
  52. });
  53. });
  54. /** used to run reset on all used sequelize instances for a given suite */
  55. const allSequelizeInstances = new Set<Sequelize>();
  56. const sequelizeInstanceSources = new WeakMap<Sequelize, string>();
  57. Sequelize.hooks.addListener('afterInit', sequelizeInstance => {
  58. allSequelizeInstances.add(sequelizeInstance);
  59. sequelizeInstanceSources.set(
  60. sequelizeInstance,
  61. new Error('A Sequelize instance was created here').stack!,
  62. );
  63. });
  64. const singleTestInstances = new Set<Sequelize>();
  65. /**
  66. * Creates a sequelize instance that will be disposed of after the current test.
  67. * Can only be used within a test. For before/after hooks, use {@link createSequelizeInstance}.
  68. *
  69. * @param options
  70. */
  71. export function createSingleTestSequelizeInstance<
  72. Dialect extends AbstractDialect = AbstractDialect,
  73. >(options?: Omit<Options<Dialect>, 'dialect'>): Sequelize {
  74. const instance = createSequelizeInstance(options);
  75. destroySequelizeAfterTest(instance);
  76. return instance;
  77. }
  78. export function destroySequelizeAfterTest(sequelizeInstance: Sequelize): void {
  79. singleTestInstances.add(sequelizeInstance);
  80. }
  81. /**
  82. * Creates a Sequelize instance to use in transaction-related tests.
  83. * You must dispose of this instance manually.
  84. *
  85. * If you're creating the instance within a test, consider using {@link createSingleTransactionalTestSequelizeInstance}.
  86. *
  87. * @param sequelizeOrOptions
  88. * @param overrideOptions
  89. */
  90. export async function createMultiTransactionalTestSequelizeInstance<
  91. Dialect extends AbstractDialect = AbstractDialect,
  92. >(
  93. sequelizeOrOptions: Sequelize | Options<Dialect>,
  94. overrideOptions?: Partial<Options<Dialect>>,
  95. ): Promise<Sequelize> {
  96. const baseOptions =
  97. sequelizeOrOptions instanceof Sequelize ? sequelizeOrOptions.rawOptions : sequelizeOrOptions;
  98. const dialect = getTestDialect();
  99. if (dialect === 'sqlite3') {
  100. const p = getSqliteDatabasePath(`transactional-${rand()}.sqlite`);
  101. if (fs.existsSync(p)) {
  102. fs.unlinkSync(p);
  103. }
  104. const _sequelize = createSequelizeInstance<SqliteDialect>({
  105. ...(baseOptions as Options<SqliteDialect>),
  106. storage: p,
  107. // allow using multiple connections as we are connecting to a file
  108. pool: { max: 5, idle: 30_000 },
  109. ...(overrideOptions as Options<SqliteDialect>),
  110. });
  111. await _sequelize.sync({ force: true });
  112. return _sequelize;
  113. }
  114. return createSequelizeInstance({
  115. ...baseOptions,
  116. ...overrideOptions,
  117. });
  118. }
  119. /**
  120. * Creates a sequelize instance to use in transaction-related tests.
  121. * This instance will be disposed of after the current test.
  122. *
  123. * Can only be used within a test. For before/after hooks, use {@link createMultiTransactionalTestSequelizeInstance}.
  124. *
  125. * @param sequelizeOrOptions
  126. * @param overrideOptions
  127. */
  128. export async function createSingleTransactionalTestSequelizeInstance<
  129. Dialect extends AbstractDialect = AbstractDialect,
  130. >(
  131. sequelizeOrOptions: Sequelize | Options<Dialect>,
  132. overrideOptions?: Partial<Options<Dialect>>,
  133. ): Promise<Sequelize> {
  134. const instance = await createMultiTransactionalTestSequelizeInstance(
  135. sequelizeOrOptions,
  136. overrideOptions,
  137. );
  138. destroySequelizeAfterTest(instance);
  139. return instance;
  140. }
  141. before('first database reset', async () => {
  142. // Reset the DB a single time for the whole suite
  143. await clearDatabase();
  144. });
  145. // TODO: make "none" the default.
  146. type ResetMode = 'none' | 'truncate' | 'destroy' | 'drop';
  147. let currentSuiteResetMode: ResetMode = 'drop';
  148. /**
  149. * Controls how the current test suite will reset the database between each test.
  150. * Note that this does not affect how the database is reset between each suite, only between each test.
  151. *
  152. * @param mode The reset mode to use:
  153. * - `drop`: All tables will be dropped and recreated (default).
  154. * - `none`: The database will not be reset at all.
  155. * - `truncate`: All tables will be truncated, but not dropped.
  156. * - `destroy`: All rows of all tables will be deleted using DELETE FROM, and identity columns will be reset.
  157. */
  158. export function setResetMode(mode: ResetMode) {
  159. let previousMode: ResetMode | undefined;
  160. before('setResetMode before', async () => {
  161. previousMode = currentSuiteResetMode;
  162. currentSuiteResetMode = mode;
  163. });
  164. after('setResetMode after', async () => {
  165. currentSuiteResetMode = previousMode ?? 'drop';
  166. // Reset the DB a single time for the whole suite
  167. await clearDatabase();
  168. });
  169. }
  170. afterEach('database reset', async () => {
  171. const sequelizeInstances = uniq([sequelize, ...allSequelizeInstances]);
  172. for (const sequelizeInstance of sequelizeInstances) {
  173. if (sequelizeInstance.isClosed()) {
  174. allSequelizeInstances.delete(sequelizeInstance);
  175. continue;
  176. }
  177. if (currentSuiteResetMode === 'none') {
  178. continue;
  179. }
  180. let hasValidCredentials;
  181. try {
  182. /* eslint-disable no-await-in-loop */
  183. await sequelizeInstance.authenticate();
  184. hasValidCredentials = true;
  185. } catch {
  186. hasValidCredentials = false;
  187. }
  188. if (hasValidCredentials) {
  189. /* eslint-disable no-await-in-loop */
  190. switch (currentSuiteResetMode) {
  191. case 'drop':
  192. await clearDatabase(sequelizeInstance);
  193. // unregister all models
  194. resetSequelizeInstance(sequelizeInstance);
  195. break;
  196. case 'truncate':
  197. await sequelizeInstance.truncate({
  198. ...sequelizeInstance.dialect.supports.truncate,
  199. withoutForeignKeyChecks:
  200. sequelizeInstance.dialect.supports.constraints.foreignKeyChecksDisableable,
  201. });
  202. break;
  203. case 'destroy':
  204. await sequelizeInstance.destroyAll({ force: true });
  205. break;
  206. default:
  207. break;
  208. /* eslint-enable no-await-in-loop */
  209. }
  210. }
  211. }
  212. if (sequelize.isClosed()) {
  213. throw new Error('The main sequelize instance was closed. This is not allowed.');
  214. }
  215. await Promise.all(
  216. [...singleTestInstances].map(async instance => {
  217. allSequelizeInstances.delete(instance);
  218. if (!instance.isClosed()) {
  219. await instance.close();
  220. }
  221. }),
  222. );
  223. singleTestInstances.clear();
  224. if (allSequelizeInstances.size > 2) {
  225. throw new Error(`There are more than two test-specific sequelize instance. This indicates that some sequelize instances were not closed.
  226. Sequelize instances created in beforeEach/before must be closed in a corresponding afterEach/after block.
  227. Sequelize instances created inside of a test must be closed after the test.
  228. The following methods can be used to mark a sequelize instance for automatic disposal:
  229. - destroySequelizeAfterTest
  230. - createSingleTransactionalTestSequelizeInstance
  231. - createSingleTestSequelizeInstance
  232. - sequelize.close()
  233. The sequelize instances were created in the following locations:
  234. ${[...allSequelizeInstances]
  235. .map(instance => {
  236. const source = sequelizeInstanceSources.get(instance);
  237. return source ? ` - ${source}` : ' - unknown';
  238. })
  239. .join('\n')}
  240. `);
  241. }
  242. });
  243. async function clearDatabaseInternal(customSequelize: Sequelize) {
  244. const qi = customSequelize.queryInterface;
  245. await qi.dropAllTables();
  246. resetSequelizeInstance(customSequelize);
  247. if (qi.dropAllEnums) {
  248. await qi.dropAllEnums();
  249. }
  250. await dropTestSchemas(customSequelize);
  251. await dropTestDatabases(customSequelize);
  252. }
  253. export async function clearDatabase(customSequelize: Sequelize = sequelize) {
  254. await pTimeout(
  255. clearDatabaseInternal(customSequelize),
  256. CLEANUP_TIMEOUT,
  257. `Could not clear database after this test in less than ${CLEANUP_TIMEOUT}ms. This test crashed the DB, and testing cannot continue. Aborting.`,
  258. { customTimers: { setTimeout, clearTimeout } },
  259. );
  260. }
  261. afterEach('no running queries checker', () => {
  262. if (runningQueries.size > 0) {
  263. throw new Error(
  264. `Expected 0 queries running after this test, but there are still ${
  265. runningQueries.size
  266. } queries running in the database (or, at least, the \`afterQuery\` Sequelize hook did not fire for them):\n\n${[
  267. ...runningQueries,
  268. ]
  269. .map((query: AbstractQuery) => ` ${query.uuid}: ${query.sql}`)
  270. .join('\n')}`,
  271. );
  272. }
  273. });
  274. export async function dropTestDatabases(customSequelize: Sequelize = sequelize) {
  275. if (!customSequelize.dialect.supports.multiDatabases) {
  276. return;
  277. }
  278. const qi = customSequelize.queryInterface;
  279. const databases = await qi.listDatabases({
  280. skip: [customSequelize.dialect.getDefaultSchema(), 'sequelize_test'],
  281. });
  282. if (getTestDialect() === 'db2') {
  283. for (const db of databases) {
  284. // DB2 can sometimes deadlock / timeout when deleting more than one schema at the same time.
  285. // eslint-disable-next-line no-await-in-loop
  286. await qi.dropDatabase(db.name);
  287. }
  288. } else {
  289. await Promise.all(databases.map(async db => qi.dropDatabase(db.name)));
  290. }
  291. }
  292. export async function dropTestSchemas(customSequelize: Sequelize = sequelize) {
  293. if (!customSequelize.dialect.supports.schemas) {
  294. await customSequelize.drop({});
  295. return;
  296. }
  297. await customSequelize.queryInterface.dropAllSchemas({
  298. skip: [customSequelize.dialect.getDefaultSchema(), 'sequelize_test'],
  299. });
  300. }
  301. export * from '../support';