database-transactions.e2e-spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import { mergeConfig } from '@vendure/core';
  2. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  3. import { fail } from 'assert';
  4. import path from 'path';
  5. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  6. import { initialData } from '../../../e2e-common/e2e-initial-data';
  7. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  8. import {
  9. TransactionTestPlugin,
  10. TRIGGER_ATTEMPTED_READ_EMAIL,
  11. TRIGGER_ATTEMPTED_UPDATE_EMAIL,
  12. TRIGGER_NO_OPERATION,
  13. } from './fixtures/test-plugins/transaction-test-plugin';
  14. import { FragmentOf, graphql, ResultOf } from './graphql/graphql-admin';
  15. type DBType = 'mysql' | 'postgres' | 'sqlite' | 'sqljs';
  16. const itIfDb = (dbs: DBType[]) => {
  17. return dbs.includes((process.env.DB as DBType) || 'sqljs') ? it : it.skip;
  18. };
  19. describe('Transaction infrastructure', () => {
  20. const { server, adminClient } = createTestEnvironment(
  21. mergeConfig(testConfig(), {
  22. plugins: [TransactionTestPlugin],
  23. }),
  24. );
  25. beforeAll(async () => {
  26. await server.init({
  27. initialData,
  28. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  29. customerCount: 0,
  30. });
  31. await adminClient.asSuperAdmin();
  32. }, TEST_SETUP_TIMEOUT_MS);
  33. afterAll(async () => {
  34. await server.destroy();
  35. });
  36. it('non-failing mutation', async () => {
  37. const { createTestAdministrator } = await adminClient.query(createTestAdministratorDocument, {
  38. emailAddress: 'test1',
  39. fail: false,
  40. });
  41. createdAdminGuard.assertSuccess(createTestAdministrator);
  42. expect(createTestAdministrator.emailAddress).toBe('test1');
  43. expect(createTestAdministrator.user.identifier).toBe('test1');
  44. const { verify } = await adminClient.query(verifyTestDocument);
  45. expect(verify.admins.length).toBe(2);
  46. expect(verify.users.length).toBe(2);
  47. expect(!!verify.admins.find(a => a.emailAddress === 'test1')).toBe(true);
  48. expect(!!verify.users.find(u => u.identifier === 'test1')).toBe(true);
  49. });
  50. it('failing mutation', async () => {
  51. try {
  52. await adminClient.query(createTestAdministratorDocument, {
  53. emailAddress: 'test2',
  54. fail: true,
  55. });
  56. fail('Should have thrown');
  57. } catch (e: any) {
  58. expect(e.message).toContain('Failed!');
  59. }
  60. const { verify } = await adminClient.query(verifyTestDocument);
  61. expect(verify.admins.length).toBe(2);
  62. expect(verify.users.length).toBe(2);
  63. expect(!!verify.admins.find(a => a.emailAddress === 'test2')).toBe(false);
  64. expect(!!verify.users.find(u => u.identifier === 'test2')).toBe(false);
  65. });
  66. it('failing mutation with promise concurrent execution', async () => {
  67. try {
  68. await adminClient.query(createNTestAdministratorsDocument, {
  69. emailAddress: 'testN-',
  70. failFactor: 0.4,
  71. n: 10,
  72. });
  73. fail('Should have thrown');
  74. } catch (e) {
  75. expect(e.message).toContain('Failed!');
  76. }
  77. const { verify } = await adminClient.query(verifyTestDocument);
  78. expect(verify.admins.length).toBe(2);
  79. expect(verify.users.length).toBe(2);
  80. expect(!!verify.admins.find(a => a.emailAddress.includes('testN'))).toBe(false);
  81. expect(!!verify.users.find(u => u.identifier.includes('testN'))).toBe(false);
  82. });
  83. it('failing manual mutation', async () => {
  84. try {
  85. await adminClient.query(createTestAdministrator2Document, {
  86. emailAddress: 'test3',
  87. fail: true,
  88. });
  89. fail('Should have thrown');
  90. } catch (e: any) {
  91. expect(e.message).toContain('Failed!');
  92. }
  93. const { verify } = await adminClient.query(verifyTestDocument);
  94. expect(verify.admins.length).toBe(2);
  95. expect(verify.users.length).toBe(2);
  96. expect(!!verify.admins.find(a => a.emailAddress === 'test3')).toBe(false);
  97. expect(!!verify.users.find(u => u.identifier === 'test3')).toBe(false);
  98. });
  99. it('failing manual mutation without transaction', async () => {
  100. try {
  101. await adminClient.query(createTestAdministrator3Document, {
  102. emailAddress: 'test4',
  103. fail: true,
  104. });
  105. fail('Should have thrown');
  106. } catch (e: any) {
  107. expect(e.message).toContain('Failed!');
  108. }
  109. const { verify } = await adminClient.query(verifyTestDocument);
  110. expect(verify.admins.length).toBe(2);
  111. expect(verify.users.length).toBe(3);
  112. expect(!!verify.admins.find(a => a.emailAddress === 'test4')).toBe(false);
  113. expect(!!verify.users.find(u => u.identifier === 'test4')).toBe(true);
  114. });
  115. it('failing mutation inside connection.withTransaction() wrapper with request context', async () => {
  116. try {
  117. await adminClient.query(createTestAdministrator5Document, {
  118. emailAddress: 'test5',
  119. fail: true,
  120. noContext: false,
  121. });
  122. fail('Should have thrown');
  123. } catch (e: any) {
  124. expect(e.message).toContain('Failed!');
  125. }
  126. const { verify } = await adminClient.query(verifyTestDocument);
  127. expect(verify.admins.length).toBe(2);
  128. expect(verify.users.length).toBe(3);
  129. expect(!!verify.admins.find(a => a.emailAddress === 'test5')).toBe(false);
  130. expect(!!verify.users.find(u => u.identifier === 'test5')).toBe(false);
  131. });
  132. itIfDb(['postgres', 'mysql'])(
  133. 'failing mutation inside connection.withTransaction() wrapper with context and promise concurrent execution',
  134. async () => {
  135. try {
  136. await adminClient.query(createNTestAdministrators2Document, {
  137. emailAddress: 'testN-',
  138. failFactor: 0.4,
  139. n: 10,
  140. });
  141. fail('Should have thrown');
  142. } catch (e) {
  143. expect(e.message).toMatch(
  144. /^Failed!|Query runner already released. Cannot run queries anymore.$/,
  145. );
  146. }
  147. const { verify } = await adminClient.query(verifyTestDocument);
  148. expect(verify.admins.length).toBe(2);
  149. expect(verify.users.length).toBe(3);
  150. expect(!!verify.admins.find(a => a.emailAddress.includes('testN'))).toBe(false);
  151. expect(!!verify.users.find(u => u.identifier.includes('testN'))).toBe(false);
  152. },
  153. );
  154. it('failing mutation inside connection.withTransaction() wrapper without request context', async () => {
  155. try {
  156. await adminClient.query(createTestAdministrator5Document, {
  157. emailAddress: 'test5',
  158. fail: true,
  159. noContext: true,
  160. });
  161. fail('Should have thrown');
  162. } catch (e: any) {
  163. expect(e.message).toContain('Failed!');
  164. }
  165. const { verify } = await adminClient.query(verifyTestDocument);
  166. expect(verify.admins.length).toBe(2);
  167. expect(verify.users.length).toBe(3);
  168. expect(!!verify.admins.find(a => a.emailAddress === 'test5')).toBe(false);
  169. expect(!!verify.users.find(u => u.identifier === 'test5')).toBe(false);
  170. });
  171. it('non-failing mutation inside connection.withTransaction() wrapper with failing nested transactions and request context', async () => {
  172. await adminClient.query(createNTestAdministrators3Document, {
  173. emailAddress: 'testNestedTransactionsN-',
  174. failFactor: 0.5,
  175. n: 2,
  176. });
  177. const { verify } = await adminClient.query(verifyTestDocument);
  178. expect(verify.admins.length).toBe(3);
  179. expect(verify.users.length).toBe(4);
  180. expect(verify.admins.filter(a => a.emailAddress.includes('testNestedTransactionsN'))).toHaveLength(1);
  181. expect(verify.users.filter(u => u.identifier.includes('testNestedTransactionsN'))).toHaveLength(1);
  182. });
  183. it('event do not publish after transaction rollback', async () => {
  184. TransactionTestPlugin.reset();
  185. try {
  186. await adminClient.query(createNTestAdministratorsDocument, {
  187. emailAddress: TRIGGER_NO_OPERATION,
  188. failFactor: 0.5,
  189. n: 2,
  190. });
  191. fail('Should have thrown');
  192. } catch (e) {
  193. expect(e.message).toContain('Failed!');
  194. }
  195. // Wait a bit to see an events in handler
  196. await new Promise(resolve => setTimeout(resolve, 100));
  197. expect(TransactionTestPlugin.callHandler).not.toHaveBeenCalled();
  198. expect(TransactionTestPlugin.errorHandler).not.toHaveBeenCalled();
  199. });
  200. // Testing https://github.com/vendure-ecommerce/vendure/issues/520
  201. it('passing transaction via EventBus', async () => {
  202. TransactionTestPlugin.reset();
  203. const { createTestAdministrator } = await adminClient.query(createTestAdministratorDocument, {
  204. emailAddress: TRIGGER_ATTEMPTED_UPDATE_EMAIL,
  205. fail: false,
  206. });
  207. createdAdminGuard.assertSuccess(createTestAdministrator);
  208. await TransactionTestPlugin.eventHandlerComplete$.toPromise();
  209. expect(createTestAdministrator.emailAddress).toBe(TRIGGER_ATTEMPTED_UPDATE_EMAIL);
  210. expect(TransactionTestPlugin.errorHandler).not.toHaveBeenCalled();
  211. });
  212. // Testing https://github.com/vendure-ecommerce/vendure/issues/1107
  213. it('passing transaction via EventBus with delay in committing transaction', async () => {
  214. TransactionTestPlugin.reset();
  215. const { createTestAdministrator4 } = await adminClient.query(createTestAdministrator4Document, {
  216. emailAddress: TRIGGER_ATTEMPTED_READ_EMAIL,
  217. fail: false,
  218. });
  219. createdAdminGuard.assertSuccess(createTestAdministrator4);
  220. await TransactionTestPlugin.eventHandlerComplete$.toPromise();
  221. expect(createTestAdministrator4.emailAddress).toBe(TRIGGER_ATTEMPTED_READ_EMAIL);
  222. expect(TransactionTestPlugin.errorHandler).not.toHaveBeenCalled();
  223. });
  224. });
  225. const createdAdminFragment = graphql(`
  226. fragment CreatedAdmin on Administrator {
  227. id
  228. emailAddress
  229. user {
  230. id
  231. identifier
  232. }
  233. }
  234. `);
  235. type CreatedAdmin = FragmentOf<typeof createdAdminFragment>;
  236. const createdAdminGuard: ErrorResultGuard<CreatedAdmin> = createErrorResultGuard(input => !!input.id);
  237. const createTestAdministratorDocument = graphql(
  238. `
  239. mutation CreateTestAdmin($emailAddress: String!, $fail: Boolean!) {
  240. createTestAdministrator(emailAddress: $emailAddress, fail: $fail) {
  241. ...CreatedAdmin
  242. }
  243. }
  244. `,
  245. [createdAdminFragment],
  246. );
  247. const createTestAdministrator2Document = graphql(
  248. `
  249. mutation CreateTestAdmin2($emailAddress: String!, $fail: Boolean!) {
  250. createTestAdministrator2(emailAddress: $emailAddress, fail: $fail) {
  251. ...CreatedAdmin
  252. }
  253. }
  254. `,
  255. [createdAdminFragment],
  256. );
  257. const createTestAdministrator3Document = graphql(
  258. `
  259. mutation CreateTestAdmin3($emailAddress: String!, $fail: Boolean!) {
  260. createTestAdministrator3(emailAddress: $emailAddress, fail: $fail) {
  261. ...CreatedAdmin
  262. }
  263. }
  264. `,
  265. [createdAdminFragment],
  266. );
  267. const createTestAdministrator4Document = graphql(
  268. `
  269. mutation CreateTestAdmin4($emailAddress: String!, $fail: Boolean!) {
  270. createTestAdministrator4(emailAddress: $emailAddress, fail: $fail) {
  271. ...CreatedAdmin
  272. }
  273. }
  274. `,
  275. [createdAdminFragment],
  276. );
  277. const createTestAdministrator5Document = graphql(
  278. `
  279. mutation CreateTestAdmin5($emailAddress: String!, $fail: Boolean!, $noContext: Boolean!) {
  280. createTestAdministrator5(emailAddress: $emailAddress, fail: $fail, noContext: $noContext) {
  281. ...CreatedAdmin
  282. }
  283. }
  284. `,
  285. [createdAdminFragment],
  286. );
  287. const createNTestAdministratorsDocument = graphql(`
  288. mutation CreateNTestAdmins($emailAddress: String!, $failFactor: Float!, $n: Int!) {
  289. createNTestAdministrators(emailAddress: $emailAddress, failFactor: $failFactor, n: $n)
  290. }
  291. `);
  292. const createNTestAdministrators2Document = graphql(`
  293. mutation CreateNTestAdmins2($emailAddress: String!, $failFactor: Float!, $n: Int!) {
  294. createNTestAdministrators2(emailAddress: $emailAddress, failFactor: $failFactor, n: $n)
  295. }
  296. `);
  297. const createNTestAdministrators3Document = graphql(`
  298. mutation CreateNTestAdmins3($emailAddress: String!, $failFactor: Float!, $n: Int!) {
  299. createNTestAdministrators3(emailAddress: $emailAddress, failFactor: $failFactor, n: $n)
  300. }
  301. `);
  302. const verifyTestDocument = graphql(`
  303. query VerifyTest {
  304. verify {
  305. admins {
  306. id
  307. emailAddress
  308. }
  309. users {
  310. id
  311. identifier
  312. }
  313. }
  314. }
  315. `);
  316. type VerifyResult = ResultOf<typeof verifyTestDocument>;