http.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { ipcMain } from 'electron'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { http_api_port } from '../../src/common/constants'
  4. import events from '../../src/common/events'
  5. import { setList } from '../../src/main/actions'
  6. import { clearData } from '../_base'
  7. const { closeMock, serveMock } = vi.hoisted(() => {
  8. const close = vi.fn()
  9. const serve = vi.fn((options: object, callback?: () => void) => {
  10. callback?.()
  11. return {
  12. close,
  13. options,
  14. }
  15. })
  16. return {
  17. closeMock: close,
  18. serveMock: serve,
  19. }
  20. })
  21. vi.mock('@hono/node-server', () => ({
  22. serve: serveMock,
  23. }))
  24. import { app, start, stop } from '../../src/main/http'
  25. describe('http api test', () => {
  26. beforeEach(async () => {
  27. await clearData()
  28. })
  29. afterEach(() => {
  30. vi.restoreAllMocks()
  31. serveMock.mockClear()
  32. closeMock.mockClear()
  33. })
  34. it('should log request metadata for incoming requests', async () => {
  35. const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
  36. const response = await app.request('/api/list', {
  37. headers: {
  38. 'user-agent': 'vitest',
  39. },
  40. })
  41. expect(response.status).toBe(200)
  42. expect(logSpy).toHaveBeenCalledWith(
  43. expect.stringMatching(/^> ".+"$/),
  44. 'GET',
  45. '/api/list',
  46. '"vitest"',
  47. )
  48. })
  49. it('should respond on root endpoint', async () => {
  50. const response = await app.request('/')
  51. expect(response.status).toBe(200)
  52. expect(await response.text()).toBe('Hello SwitchHosts!')
  53. })
  54. it('should respond on remote test endpoint', async () => {
  55. const response = await app.request('/remote-test')
  56. expect(response.status).toBe(200)
  57. expect(await response.text()).toMatch(/^# remote-test\n# .+/)
  58. })
  59. it('should flatten list data for api list endpoint', async () => {
  60. await setList([
  61. { id: 'top-1', title: 'Top 1' },
  62. {
  63. id: 'folder-1',
  64. type: 'folder',
  65. children: [
  66. { id: 'child-1', title: 'Child 1' },
  67. { id: 'child-2', title: 'Child 2' },
  68. ],
  69. },
  70. ])
  71. const response = await app.request('/api/list')
  72. const body = await response.json()
  73. expect(response.status).toBe(200)
  74. expect(body.success).toBe(true)
  75. expect(body.data.map((item: { id: string }) => item.id)).toEqual([
  76. 'top-1',
  77. 'folder-1',
  78. 'child-1',
  79. 'child-2',
  80. ])
  81. })
  82. it('should reject toggle requests without id', async () => {
  83. const response = await app.request('/api/toggle')
  84. expect(response.status).toBe(200)
  85. expect(await response.text()).toBe('bad id.')
  86. })
  87. it('should return not found for unknown toggle id', async () => {
  88. const response = await app.request('/api/toggle?id=missing')
  89. expect(response.status).toBe(200)
  90. expect(await response.text()).toBe('not found.')
  91. })
  92. it('should broadcast toggle event for existing item', async () => {
  93. const emitSpy = vi.spyOn(ipcMain, 'emit')
  94. await setList([
  95. { id: 'item-1', on: false, title: 'Item 1' },
  96. ])
  97. const response = await app.request('/api/toggle?id=item-1')
  98. expect(response.status).toBe(200)
  99. expect(await response.text()).toBe('ok')
  100. expect(emitSpy).toHaveBeenCalledWith('x_broadcast', null, {
  101. event: events.toggle_item,
  102. args: [ 'item-1', true ],
  103. })
  104. })
  105. it('should listen on localhost when local-only mode is enabled', () => {
  106. expect(start(true)).toBe(true)
  107. expect(serveMock.mock.calls[0]?.[0]).toEqual({
  108. fetch: app.fetch,
  109. port: http_api_port,
  110. hostname: '127.0.0.1',
  111. })
  112. expect(typeof serveMock.mock.calls[0]?.[1]).toBe('function')
  113. stop()
  114. expect(closeMock).toHaveBeenCalledOnce()
  115. })
  116. it('should listen on all interfaces when local-only mode is disabled', () => {
  117. expect(start(false)).toBe(true)
  118. expect(serveMock.mock.calls[0]?.[0]).toEqual({
  119. fetch: app.fetch,
  120. port: http_api_port,
  121. hostname: '0.0.0.0',
  122. })
  123. expect(typeof serveMock.mock.calls[0]?.[1]).toBe('function')
  124. stop()
  125. expect(closeMock).toHaveBeenCalledOnce()
  126. })
  127. it('should return false when serve throws', () => {
  128. const error = new Error('listen failed')
  129. serveMock.mockImplementationOnce(() => {
  130. throw error
  131. })
  132. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  133. expect(start(true)).toBe(false)
  134. expect(errorSpy).toHaveBeenCalledWith(error)
  135. })
  136. it('should swallow close errors when stopping server', () => {
  137. const error = new Error('close failed')
  138. const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
  139. const failingClose = vi.fn(() => {
  140. throw error
  141. })
  142. serveMock.mockImplementationOnce((options: object, callback?: () => void) => {
  143. callback?.()
  144. return {
  145. close: failingClose,
  146. options,
  147. }
  148. })
  149. expect(start(true)).toBe(true)
  150. stop()
  151. expect(errorSpy).toHaveBeenCalledWith(error)
  152. })
  153. })