AbstractMockApi.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import OError from '@overleaf/o-error'
  2. import express from 'express'
  3. import bodyParser from 'body-parser'
  4. /**
  5. * Abstract class for running a mock API via Express. Handles setting up of
  6. * the server on a specific port, and provides an overridable method to
  7. * initialise routes.
  8. *
  9. * Mocks are singletons, and must be initialized with the `initialize` method.
  10. * Instance objects are available via the `instance()` method.
  11. *
  12. * You must override 'reset' and 'applyRoutes' when subclassing this
  13. *
  14. * Wraps the express app's http verb methods for convenience
  15. *
  16. * @hideconstructor
  17. * @member {number} port - the port for the http server
  18. * @member app - the Express application
  19. */
  20. class AbstractMockApi {
  21. /**
  22. * Create a new API. No not call directly - use the `initialize` method
  23. *
  24. * @param {number} port - The TCP port to start the API on
  25. * @param {object} options - An optional hash of options to modify the behaviour of the mock
  26. * @param {boolean} options.debug - When true, print http requests and responses to stdout
  27. * Set this to 'true' from the constructor of your derived class
  28. */
  29. constructor(port, { debug } = {}) {
  30. if (!this.constructor._fromInit) {
  31. throw new OError(
  32. 'do not create this class directly - use the initialize method',
  33. { className: this.constructor.name }
  34. )
  35. }
  36. if (this.constructor._obj) {
  37. throw new OError('mock already initialized', {
  38. className: this.constructor._obj.constructor.name,
  39. port: this.port,
  40. })
  41. }
  42. if (this.constructor === AbstractMockApi) {
  43. throw new OError(
  44. 'Do not construct AbstractMockApi directly - use a subclass'
  45. )
  46. }
  47. this.debug = debug
  48. this.port = port
  49. this.app = express()
  50. this.app.use(bodyParser.json())
  51. this.app.use(bodyParser.urlencoded({ extended: true }))
  52. }
  53. /**
  54. * Apply debugging routes to print out API activity to stdout
  55. */
  56. applyDebugRoutes() {
  57. if (!this.debug) return
  58. this.app.use((req, res, next) => {
  59. const { method, path, query, params, body } = req
  60. // eslint-disable-next-line no-console
  61. console.log(`${this.constructor.name} REQUEST`, {
  62. method,
  63. path,
  64. query,
  65. params,
  66. body,
  67. })
  68. const oldEnd = res.end
  69. const oldJson = res.json
  70. res.json = (...args) => {
  71. // eslint-disable-next-line no-console
  72. console.log(`${this.constructor.name} RESPONSE JSON`, args[0])
  73. oldJson.call(res, ...args)
  74. }
  75. res.end = (...args) => {
  76. // eslint-disable-next-line no-console
  77. console.log(`${this.constructor.name} STATUS`, res.statusCode)
  78. if (res.statusCode >= 500) {
  79. // eslint-disable-next-line no-console
  80. console.log('ERROR RESPONSE:', args)
  81. }
  82. oldEnd.call(res, ...args)
  83. }
  84. next()
  85. })
  86. }
  87. /**
  88. * Overridable method to add routes - should be overridden in derived classes
  89. * @abstract
  90. */
  91. applyRoutes() {
  92. throw new OError(
  93. 'AbstractMockApi base class implementation should not be called'
  94. )
  95. }
  96. /**
  97. * Resets member data and restores the API to a clean state for the next test run
  98. * - may be overridden in derived classes
  99. */
  100. reset() {}
  101. /**
  102. * Applies mocha hooks to start and stop the API at the beginning/end of
  103. * the test suite, and reset before each test run
  104. *
  105. * @param {number} port - The TCP port to start the API on
  106. * @param {object} options - An optional hash of options to modify the behaviour of the mock
  107. * @param {boolean} options.debug - When true, print http requests and responses to stdout
  108. * Set this to 'true' from the constructor of your derived class
  109. */
  110. static initialize(port, { debug } = {}) {
  111. // `this` refers to the derived class
  112. this._fromInit = true
  113. this._obj = new this(port, { debug })
  114. this._obj.applyDebugRoutes()
  115. this._obj.applyRoutes()
  116. /* eslint-disable mocha/no-mocha-arrows */
  117. const name = this.constructor.name
  118. before(`starting mock ${name}`, () => this._obj.start())
  119. after(`stopping mock ${name}`, () => this._obj.stop())
  120. beforeEach(`resetting mock ${name}`, () => this._obj.reset())
  121. }
  122. /**
  123. * Starts the API on the configured port
  124. *
  125. * @return {Promise<void>}
  126. */
  127. async start() {
  128. return new Promise((resolve, reject) => {
  129. if (this.debug) {
  130. // eslint-disable-next-line no-console
  131. console.log('Starting mock on port', this.constructor.name, this.port)
  132. }
  133. this.server = this.app
  134. .listen(this.port, '127.0.0.1', err => {
  135. if (err) {
  136. return reject(err)
  137. }
  138. resolve()
  139. })
  140. .on('error', error => {
  141. // eslint-disable-next-line no-console
  142. console.error(
  143. 'error starting mock:',
  144. this.constructor.name,
  145. error.message
  146. )
  147. process.exit(1)
  148. })
  149. })
  150. }
  151. /**
  152. * Returns the constructed object
  153. *
  154. * @return {AbstractMockApi}
  155. */
  156. static instance() {
  157. return this._obj
  158. }
  159. /**
  160. * Shuts down the API and waits for it to stop listening
  161. *
  162. * @return {Promise<void>}
  163. */
  164. async stop() {
  165. if (!this.server) return
  166. return new Promise((resolve, reject) => {
  167. if (this.debug) {
  168. // eslint-disable-next-line no-console
  169. console.log('Stopping mock', this.constructor.name)
  170. }
  171. this.server.close(err => {
  172. delete this.server
  173. if (err) {
  174. return reject(err)
  175. }
  176. resolve()
  177. })
  178. })
  179. }
  180. }
  181. export default AbstractMockApi