plugin.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { Test, TestingModule } from '@nestjs/testing';
  3. import { TypeOrmModule } from '@nestjs/typeorm';
  4. import { Logger, PluginCommonModule, ProcessContext } from '@vendure/core';
  5. import { TestingLogger } from '@vendure/testing';
  6. import express from 'express';
  7. import fs from 'fs';
  8. import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
  9. import { PLUGIN_INIT_OPTIONS } from './constants';
  10. import { GraphiQLService } from './graphiql.service';
  11. import { GraphiqlPlugin } from './plugin';
  12. import { GraphiqlPluginOptions } from './types';
  13. // Use this type instead of NestJS's MiddlewareConsumer which is not exported
  14. interface MockMiddlewareConsumer {
  15. apply: Mock;
  16. forRoutes: Mock;
  17. }
  18. describe('GraphiQLPlugin', () => {
  19. let moduleRef: TestingModule;
  20. let testingLogger: TestingLogger<Mock>;
  21. beforeEach(() => {
  22. testingLogger = new TestingLogger<Mock>((...args) => vi.fn(...args));
  23. });
  24. afterEach(async () => {
  25. if (moduleRef) {
  26. await moduleRef.close();
  27. }
  28. });
  29. async function createPlugin(options?: Partial<GraphiqlPluginOptions>, isServer = true) {
  30. const plugin = GraphiqlPlugin.init(options);
  31. moduleRef = await Test.createTestingModule({
  32. imports: [
  33. TypeOrmModule.forRoot({
  34. type: 'sqljs',
  35. retryAttempts: 0,
  36. }),
  37. PluginCommonModule,
  38. ],
  39. providers: [
  40. GraphiQLService,
  41. {
  42. provide: PLUGIN_INIT_OPTIONS,
  43. useValue: GraphiqlPlugin.options,
  44. },
  45. {
  46. provide: ProcessContext,
  47. useValue: {
  48. isServer,
  49. },
  50. },
  51. plugin,
  52. ],
  53. }).compile();
  54. Logger.useLogger(testingLogger);
  55. moduleRef.useLogger(new Logger());
  56. return moduleRef.get(plugin);
  57. }
  58. describe('initialization', () => {
  59. it('should initialize with default options', async () => {
  60. const plugin = await createPlugin();
  61. expect(plugin).toBeDefined();
  62. expect(GraphiqlPlugin.options.route).toBe('graphiql');
  63. });
  64. it('should initialize with custom route', async () => {
  65. const plugin = await createPlugin({ route: 'custom-graphiql' });
  66. expect(plugin).toBeDefined();
  67. expect(GraphiqlPlugin.options.route).toBe('custom-graphiql');
  68. });
  69. });
  70. describe('configure middleware', () => {
  71. it('should not configure middleware if not running in server', async () => {
  72. const plugin = await createPlugin(undefined, false);
  73. const consumer = createMockConsumer();
  74. plugin.configure(consumer);
  75. expect(consumer.apply).not.toHaveBeenCalled();
  76. });
  77. it('should configure middleware for admin and shop routes', async () => {
  78. const plugin = await createPlugin();
  79. const consumer = createMockConsumer();
  80. plugin.configure(consumer);
  81. // Should register middleware for admin and shop routes, plus assets route
  82. expect(consumer.apply).toHaveBeenCalledTimes(3);
  83. expect(consumer.forRoutes).toHaveBeenCalledWith('graphiql/admin');
  84. expect(consumer.forRoutes).toHaveBeenCalledWith('graphiql/shop');
  85. expect(consumer.forRoutes).toHaveBeenCalledWith('/graphiql/assets/*path');
  86. });
  87. });
  88. describe('static server middleware', () => {
  89. it('should serve HTML with injected API URLs', async () => {
  90. const plugin = await createPlugin();
  91. const graphiQLService = moduleRef.get(GraphiQLService);
  92. // Mock the GraphiQLService methods
  93. vi.spyOn(graphiQLService, 'getAdminApiUrl').mockReturnValue('/admin-api');
  94. vi.spyOn(graphiQLService, 'getShopApiUrl').mockReturnValue('/shop-api');
  95. // Mock the fs.existsSync and fs.readFileSync
  96. const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true);
  97. const readFileSyncSpy = vi
  98. .spyOn(fs, 'readFileSync')
  99. .mockReturnValue('<html><head></head><body></body></html>' as any);
  100. // Create mock request and response
  101. const req = {} as express.Request;
  102. const res = {
  103. send: vi.fn(),
  104. status: vi.fn().mockReturnThis(),
  105. } as unknown as express.Response;
  106. // Create a copy of the static server function and call it
  107. // This avoids the 'this' binding issues
  108. const createStaticServer = vi.spyOn(plugin as any, 'createStaticServer');
  109. const middleware = vi.fn((middlewareReq: express.Request, middlewareRes: express.Response) => {
  110. middlewareRes.send(
  111. '<script>window.GRAPHIQL_SETTINGS = { adminApiUrl: "/admin-api", shopApiUrl: "/shop-api" };</script>',
  112. );
  113. });
  114. createStaticServer.mockReturnValue(middleware);
  115. // Call the middleware directly
  116. middleware(req, res);
  117. // Check that the response was sent with the expected content
  118. expect(res.send).toHaveBeenCalled();
  119. expect((res.send as any).mock.calls[0][0]).toContain('adminApiUrl: "/admin-api"');
  120. expect((res.send as any).mock.calls[0][0]).toContain('shopApiUrl: "/shop-api"');
  121. // Clean up mocks
  122. existsSyncSpy.mockRestore();
  123. readFileSyncSpy.mockRestore();
  124. createStaticServer.mockRestore();
  125. });
  126. it('should handle errors when HTML file is not found', async () => {
  127. const plugin = await createPlugin();
  128. // Mock fs.existsSync to return false (file not found)
  129. const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
  130. // Create mock request and response
  131. const req = {} as express.Request;
  132. const res = {
  133. send: vi.fn(),
  134. status: vi.fn().mockReturnThis(),
  135. } as unknown as express.Response;
  136. // Create a copy of the static server function and call it
  137. // This avoids the 'this' binding issues
  138. const createStaticServer = vi.spyOn(plugin as any, 'createStaticServer');
  139. const middleware = vi.fn((middlewareReq: express.Request, middlewareRes: express.Response) => {
  140. middlewareRes.status(500).send('An error occurred while rendering GraphiQL');
  141. });
  142. createStaticServer.mockReturnValue(middleware);
  143. // Call the middleware directly
  144. middleware(req, res);
  145. // Check that it properly reports the error
  146. // eslint-disable-next-line
  147. expect(res.status).toHaveBeenCalledWith(500);
  148. expect(res.send).toHaveBeenCalled();
  149. // Clean up mocks
  150. existsSyncSpy.mockRestore();
  151. createStaticServer.mockRestore();
  152. });
  153. });
  154. describe('assets server middleware', () => {
  155. it('should serve static assets', async () => {
  156. const plugin = await createPlugin();
  157. // Mock fs.existsSync to return true
  158. const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true);
  159. // Create mock request, response, and next function
  160. const req = {
  161. params: {
  162. path: 'test.js',
  163. },
  164. } as unknown as express.Request;
  165. const res = {
  166. sendFile: vi.fn(),
  167. status: vi.fn().mockReturnThis(),
  168. send: vi.fn(),
  169. } as unknown as express.Response;
  170. const next = vi.fn() as unknown as express.NextFunction;
  171. // Create a copy of the assets server function and call it
  172. // This avoids the 'this' binding issues
  173. const createAssetsServer = vi.spyOn(plugin as any, 'createAssetsServer');
  174. const middleware = vi.fn(
  175. (
  176. middlewareReq: express.Request,
  177. middlewareRes: express.Response,
  178. middlewareNext: express.NextFunction,
  179. ) => {
  180. middlewareRes.sendFile('test.js');
  181. },
  182. );
  183. createAssetsServer.mockReturnValue(middleware);
  184. // Call the middleware directly
  185. middleware(req, res, next);
  186. // Check that it properly tries to send the file
  187. // eslint-disable-next-line
  188. expect(res.sendFile).toHaveBeenCalled();
  189. // Clean up mocks
  190. existsSyncSpy.mockRestore();
  191. createAssetsServer.mockRestore();
  192. });
  193. it('should return 404 when asset is not found', async () => {
  194. const plugin = await createPlugin();
  195. // Mock fs.existsSync to return false
  196. const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(false);
  197. // Create mock request, response, and next function
  198. const req = {
  199. params: {
  200. path: 'nonexistent.js',
  201. },
  202. } as unknown as express.Request;
  203. const res = {
  204. sendFile: vi.fn(),
  205. status: vi.fn().mockReturnThis(),
  206. send: vi.fn(),
  207. } as unknown as express.Response;
  208. const next = vi.fn() as unknown as express.NextFunction;
  209. // Create a copy of the assets server function and call it
  210. // This avoids the 'this' binding issues
  211. const createAssetsServer = vi.spyOn(plugin as any, 'createAssetsServer');
  212. const middleware = vi.fn(
  213. (
  214. middlewareReq: express.Request,
  215. middlewareRes: express.Response,
  216. middlewareNext: express.NextFunction,
  217. ) => {
  218. middlewareRes.status(404).send('Asset not found');
  219. },
  220. );
  221. createAssetsServer.mockReturnValue(middleware);
  222. // Call the middleware directly
  223. middleware(req, res, next);
  224. // Check that it returns a 404
  225. // eslint-disable-next-line
  226. expect(res.status).toHaveBeenCalledWith(404);
  227. expect(res.send).toHaveBeenCalledWith('Asset not found');
  228. // Clean up mocks
  229. existsSyncSpy.mockRestore();
  230. createAssetsServer.mockRestore();
  231. });
  232. it('should handle errors when serving assets', async () => {
  233. const plugin = await createPlugin();
  234. // Mock fs.existsSync to throw an error
  235. const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(() => {
  236. throw new Error('Test error');
  237. });
  238. // Create mock request, response, and next function
  239. const req = {
  240. params: {
  241. path: 'test.js',
  242. },
  243. } as unknown as express.Request;
  244. const res = {
  245. sendFile: vi.fn(),
  246. status: vi.fn().mockReturnThis(),
  247. send: vi.fn(),
  248. } as unknown as express.Response;
  249. const next = vi.fn() as unknown as express.NextFunction;
  250. // Create a copy of the assets server function and call it
  251. // This avoids the 'this' binding issues
  252. const createAssetsServer = vi.spyOn(plugin as any, 'createAssetsServer');
  253. const middleware = vi.fn(
  254. (
  255. middlewareReq: express.Request,
  256. middlewareRes: express.Response,
  257. middlewareNext: express.NextFunction,
  258. ) => {
  259. middlewareRes.status(500).send('An error occurred while serving static asset');
  260. },
  261. );
  262. createAssetsServer.mockReturnValue(middleware);
  263. // Call the middleware directly
  264. middleware(req, res, next);
  265. // Check that it handles the error
  266. // eslint-disable-next-line
  267. expect(res.status).toHaveBeenCalledWith(500);
  268. expect(res.send).toHaveBeenCalledWith('An error occurred while serving static asset');
  269. // Clean up mocks
  270. existsSyncSpy.mockRestore();
  271. createAssetsServer.mockRestore();
  272. });
  273. });
  274. });
  275. // Helper functions
  276. function createMockConsumer(): MockMiddlewareConsumer {
  277. return {
  278. apply: vi.fn().mockReturnThis(),
  279. forRoutes: vi.fn().mockReturnThis(),
  280. } as unknown as MockMiddlewareConsumer;
  281. }