test-server.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { INestApplication, INestMicroservice } from '@nestjs/common';
  2. import { NestFactory } from '@nestjs/core';
  3. import { DefaultLogger, Logger, VendureConfig } from '@vendure/core';
  4. import { preBootstrapConfig } from '@vendure/core/dist/bootstrap';
  5. import fs from 'fs';
  6. import path from 'path';
  7. import { SqljsConnectionOptions } from 'typeorm/driver/sqljs/SqljsConnectionOptions';
  8. import { populateForTesting } from './data-population/populate-for-testing';
  9. import { Mutable, TestServerOptions } from './types';
  10. // tslint:disable:no-console
  11. /**
  12. * @description
  13. * A real Vendure server against which the e2e tests should be run.
  14. *
  15. * @docsCategory testing
  16. */
  17. export class TestServer {
  18. private app: INestApplication;
  19. private worker?: INestMicroservice;
  20. constructor(private vendureConfig: Required<VendureConfig>) {}
  21. /**
  22. * @description
  23. * Bootstraps an instance of Vendure server and populates the database according to the options
  24. * passed in. Should be called in the `beforeAll` function.
  25. *
  26. * The populated data is saved into an .sqlite file for each test file. On subsequent runs, this file
  27. * is loaded so that the populate step can be skipped, which speeds up the tests significantly.
  28. */
  29. async init(options: TestServerOptions): Promise<void> {
  30. const dbFilePath = this.getDbFilePath(options.dataDir);
  31. (this.vendureConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).location = dbFilePath;
  32. if (!fs.existsSync(dbFilePath)) {
  33. if (options.logging) {
  34. console.log(`Test data not found. Populating database and caching...`);
  35. }
  36. await this.populateInitialData(this.vendureConfig, options);
  37. }
  38. if (options.logging) {
  39. console.log(`Loading test data from "${dbFilePath}"`);
  40. }
  41. const [app, worker] = await this.bootstrapForTesting(this.vendureConfig);
  42. if (app) {
  43. this.app = app;
  44. } else {
  45. console.error(`Could not bootstrap app`);
  46. process.exit(1);
  47. }
  48. if (worker) {
  49. this.worker = worker;
  50. }
  51. }
  52. /**
  53. * @description
  54. * Destroy the Vendure server instance and clean up all resources.
  55. * Should be called after all tests have run, e.g. in an `afterAll` function.
  56. */
  57. async destroy() {
  58. // allow a grace period of any outstanding async tasks to complete
  59. await new Promise(resolve => global.setTimeout(resolve, 500));
  60. await this.app.close();
  61. if (this.worker) {
  62. await this.worker.close();
  63. }
  64. }
  65. private getDbFilePath(dataDir: string) {
  66. // tslint:disable-next-line:no-non-null-assertion
  67. const testFilePath = this.getCallerFilename(2);
  68. const dbFileName = path.basename(testFilePath) + '.sqlite';
  69. const dbFilePath = path.join(dataDir, dbFileName);
  70. return dbFilePath;
  71. }
  72. private getCallerFilename(depth: number) {
  73. let pst: ErrorConstructor['prepareStackTrace'];
  74. let stack: any;
  75. let file: any;
  76. let frame: any;
  77. pst = Error.prepareStackTrace;
  78. Error.prepareStackTrace = (_, _stack) => {
  79. Error.prepareStackTrace = pst;
  80. return _stack;
  81. };
  82. stack = new Error().stack;
  83. stack = stack.slice(depth + 1);
  84. do {
  85. frame = stack.shift();
  86. file = frame && frame.getFileName();
  87. } while (stack.length && file === 'module.js');
  88. return file;
  89. }
  90. /**
  91. * Populates an .sqlite database file based on the PopulateOptions.
  92. */
  93. private async populateInitialData(
  94. testingConfig: Required<VendureConfig>,
  95. options: TestServerOptions,
  96. ): Promise<void> {
  97. (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).autoSave = true;
  98. const [app, worker] = await populateForTesting(testingConfig, this.bootstrapForTesting, {
  99. logging: false,
  100. ...options,
  101. });
  102. await app.close();
  103. if (worker) {
  104. await worker.close();
  105. }
  106. (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).autoSave = false;
  107. }
  108. /**
  109. * Bootstraps an instance of the Vendure server for testing against.
  110. */
  111. private async bootstrapForTesting(
  112. userConfig: Partial<VendureConfig>,
  113. ): Promise<[INestApplication, INestMicroservice | undefined]> {
  114. const config = await preBootstrapConfig(userConfig);
  115. Logger.useLogger(config.logger);
  116. const appModule = await import('@vendure/core/dist/app.module');
  117. try {
  118. DefaultLogger.hideNestBoostrapLogs();
  119. const app = await NestFactory.create(appModule.AppModule, { cors: config.cors, logger: false });
  120. let worker: INestMicroservice | undefined;
  121. await app.listen(config.port);
  122. if (config.workerOptions.runInMainProcess) {
  123. const workerModule = await import('@vendure/core/dist/worker/worker.module');
  124. worker = await NestFactory.createMicroservice(workerModule.WorkerModule, {
  125. transport: config.workerOptions.transport,
  126. logger: new Logger(),
  127. options: config.workerOptions.options,
  128. });
  129. await worker.listenAsync();
  130. }
  131. DefaultLogger.restoreOriginalLogLevel();
  132. return [app, worker];
  133. } catch (e) {
  134. console.log(e);
  135. throw e;
  136. }
  137. }
  138. }