upgrade-handler.spec.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import type { IncomingMessage } from 'node:http';
  2. import type { Duplex } from 'node:stream';
  3. import type { IUserHasId } from '@growi/core';
  4. import { mock } from 'vitest-mock-extended';
  5. import { createUpgradeHandler } from './upgrade-handler';
  6. type AuthenticatedIncomingMessage = IncomingMessage & { user?: IUserHasId };
  7. interface MockSocket {
  8. write: ReturnType<typeof vi.fn>;
  9. destroy: ReturnType<typeof vi.fn>;
  10. }
  11. const { isAccessibleMock } = vi.hoisted(() => ({
  12. isAccessibleMock: vi.fn(),
  13. }));
  14. vi.mock('mongoose', () => ({
  15. default: {
  16. model: () => ({ isAccessiblePageByViewer: isAccessibleMock }),
  17. },
  18. }));
  19. const { sessionMiddlewareMock } = vi.hoisted(() => ({
  20. sessionMiddlewareMock: vi.fn(
  21. (_req: unknown, _res: unknown, next: () => void) => next(),
  22. ),
  23. }));
  24. vi.mock('express-session', () => ({
  25. default: () => sessionMiddlewareMock,
  26. }));
  27. vi.mock('passport', () => ({
  28. default: {
  29. initialize: () => (_req: unknown, _res: unknown, next: () => void) =>
  30. next(),
  31. session: () => (_req: unknown, _res: unknown, next: () => void) => next(),
  32. },
  33. }));
  34. const sessionConfig = {
  35. rolling: true,
  36. secret: 'test-secret',
  37. resave: false,
  38. saveUninitialized: true,
  39. cookie: { maxAge: 86400000 },
  40. genid: () => 'test-session-id',
  41. };
  42. const createMockRequest = (
  43. url: string,
  44. user?: IUserHasId,
  45. ): AuthenticatedIncomingMessage => {
  46. const req = mock<AuthenticatedIncomingMessage>();
  47. req.url = url;
  48. req.headers = { cookie: 'connect.sid=test-session' };
  49. req.user = user;
  50. return req;
  51. };
  52. const createMockSocket = (): Duplex & MockSocket => {
  53. return {
  54. write: vi.fn().mockReturnValue(true),
  55. destroy: vi.fn(),
  56. } as unknown as Duplex & MockSocket;
  57. };
  58. describe('UpgradeHandler', () => {
  59. const handleUpgrade = createUpgradeHandler(sessionConfig);
  60. it('should authorize a valid user with page access', async () => {
  61. isAccessibleMock.mockResolvedValue(true);
  62. const request = createMockRequest('/yjs/507f1f77bcf86cd799439011', {
  63. _id: 'user1',
  64. name: 'Test User',
  65. } as unknown as IUserHasId);
  66. const socket = createMockSocket();
  67. const head = Buffer.alloc(0);
  68. const result = await handleUpgrade(request, socket, head);
  69. expect(result.authorized).toBe(true);
  70. if (result.authorized) {
  71. expect(result.pageId).toBe('507f1f77bcf86cd799439011');
  72. }
  73. });
  74. it('should reject with 400 for missing/malformed URL path', async () => {
  75. const request = createMockRequest('/invalid/path');
  76. const socket = createMockSocket();
  77. const head = Buffer.alloc(0);
  78. const result = await handleUpgrade(request, socket, head);
  79. expect(result.authorized).toBe(false);
  80. if (!result.authorized) {
  81. expect(result.statusCode).toBe(400);
  82. }
  83. expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('400'));
  84. expect(socket.destroy).not.toHaveBeenCalled();
  85. });
  86. it('should reject with 403 when user has no page access', async () => {
  87. isAccessibleMock.mockResolvedValue(false);
  88. const request = createMockRequest('/yjs/507f1f77bcf86cd799439011', {
  89. _id: 'user1',
  90. name: 'Test User',
  91. } as unknown as IUserHasId);
  92. const socket = createMockSocket();
  93. const head = Buffer.alloc(0);
  94. const result = await handleUpgrade(request, socket, head);
  95. expect(result.authorized).toBe(false);
  96. if (!result.authorized) {
  97. expect(result.statusCode).toBe(403);
  98. }
  99. expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('403'));
  100. expect(socket.destroy).not.toHaveBeenCalled();
  101. });
  102. it('should reject with 401 when unauthenticated user has no page access', async () => {
  103. isAccessibleMock.mockResolvedValue(false);
  104. const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
  105. const socket = createMockSocket();
  106. const head = Buffer.alloc(0);
  107. const result = await handleUpgrade(request, socket, head);
  108. expect(result.authorized).toBe(false);
  109. if (!result.authorized) {
  110. expect(result.statusCode).toBe(401);
  111. }
  112. expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('401'));
  113. expect(socket.destroy).not.toHaveBeenCalled();
  114. });
  115. it('should allow guest user when page allows guest access', async () => {
  116. isAccessibleMock.mockResolvedValue(true);
  117. const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
  118. const socket = createMockSocket();
  119. const head = Buffer.alloc(0);
  120. const result = await handleUpgrade(request, socket, head);
  121. expect(result.authorized).toBe(true);
  122. if (result.authorized) {
  123. expect(result.pageId).toBe('507f1f77bcf86cd799439011');
  124. }
  125. });
  126. it('should reject with 401 when session middleware fails', async () => {
  127. sessionMiddlewareMock.mockImplementationOnce(
  128. (_req: unknown, _res: unknown, next: (err?: unknown) => void) =>
  129. next(new Error('session store unavailable')),
  130. );
  131. const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
  132. const socket = createMockSocket();
  133. const head = Buffer.alloc(0);
  134. const result = await handleUpgrade(request, socket, head);
  135. expect(result.authorized).toBe(false);
  136. if (!result.authorized) {
  137. expect(result.statusCode).toBe(401);
  138. }
  139. expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('401'));
  140. expect(socket.destroy).not.toHaveBeenCalled();
  141. });
  142. });