You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

search-error-handling.test.ts 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /**
  2. * Test GotoAnything search error handling mechanisms
  3. *
  4. * Main validations:
  5. * 1. @plugin search error handling when API fails
  6. * 2. Regular search (without @prefix) error handling when API fails
  7. * 3. Verify consistent error handling across different search types
  8. * 4. Ensure errors don't propagate to UI layer causing "search failed"
  9. */
  10. import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
  11. import { postMarketplace } from '@/service/base'
  12. import { fetchAppList } from '@/service/apps'
  13. import { fetchDatasets } from '@/service/datasets'
  14. // Mock API functions
  15. jest.mock('@/service/base', () => ({
  16. postMarketplace: jest.fn(),
  17. }))
  18. jest.mock('@/service/apps', () => ({
  19. fetchAppList: jest.fn(),
  20. }))
  21. jest.mock('@/service/datasets', () => ({
  22. fetchDatasets: jest.fn(),
  23. }))
  24. const mockPostMarketplace = postMarketplace as jest.MockedFunction<typeof postMarketplace>
  25. const mockFetchAppList = fetchAppList as jest.MockedFunction<typeof fetchAppList>
  26. const mockFetchDatasets = fetchDatasets as jest.MockedFunction<typeof fetchDatasets>
  27. describe('GotoAnything Search Error Handling', () => {
  28. beforeEach(() => {
  29. jest.clearAllMocks()
  30. // Suppress console.warn for clean test output
  31. jest.spyOn(console, 'warn').mockImplementation(() => {
  32. // Suppress console.warn for clean test output
  33. })
  34. })
  35. afterEach(() => {
  36. jest.restoreAllMocks()
  37. })
  38. describe('@plugin search error handling', () => {
  39. it('should return empty array when API fails instead of throwing error', async () => {
  40. // Mock marketplace API failure (403 permission denied)
  41. mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden'))
  42. const pluginAction = Actions.plugin
  43. // Directly call plugin action's search method
  44. const result = await pluginAction.search('@plugin', 'test', 'en')
  45. // Should return empty array instead of throwing error
  46. expect(result).toEqual([])
  47. expect(mockPostMarketplace).toHaveBeenCalledWith('/plugins/search/advanced', {
  48. body: {
  49. page: 1,
  50. page_size: 10,
  51. query: 'test',
  52. type: 'plugin',
  53. },
  54. })
  55. })
  56. it('should return empty array when user has no plugin data', async () => {
  57. // Mock marketplace returning empty data
  58. mockPostMarketplace.mockResolvedValue({
  59. data: { plugins: [] },
  60. })
  61. const pluginAction = Actions.plugin
  62. const result = await pluginAction.search('@plugin', '', 'en')
  63. expect(result).toEqual([])
  64. })
  65. it('should return empty array when API returns unexpected data structure', async () => {
  66. // Mock API returning unexpected data structure
  67. mockPostMarketplace.mockResolvedValue({
  68. data: null,
  69. })
  70. const pluginAction = Actions.plugin
  71. const result = await pluginAction.search('@plugin', 'test', 'en')
  72. expect(result).toEqual([])
  73. })
  74. })
  75. describe('Other search types error handling', () => {
  76. it('@app search should return empty array when API fails', async () => {
  77. // Mock app API failure
  78. mockFetchAppList.mockRejectedValue(new Error('API Error'))
  79. const appAction = Actions.app
  80. const result = await appAction.search('@app', 'test', 'en')
  81. expect(result).toEqual([])
  82. })
  83. it('@knowledge search should return empty array when API fails', async () => {
  84. // Mock knowledge API failure
  85. mockFetchDatasets.mockRejectedValue(new Error('API Error'))
  86. const knowledgeAction = Actions.knowledge
  87. const result = await knowledgeAction.search('@knowledge', 'test', 'en')
  88. expect(result).toEqual([])
  89. })
  90. })
  91. describe('Unified search entry error handling', () => {
  92. it('regular search (without @prefix) should return successful results even when partial APIs fail', async () => {
  93. // Set app and knowledge success, plugin failure
  94. mockFetchAppList.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
  95. mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
  96. mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
  97. const result = await searchAnything('en', 'test')
  98. // Should return successful results even if plugin search fails
  99. expect(result).toEqual([])
  100. expect(console.warn).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error))
  101. })
  102. it('@plugin dedicated search should return empty array when API fails', async () => {
  103. // Mock plugin API failure
  104. mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable'))
  105. const pluginAction = Actions.plugin
  106. const result = await searchAnything('en', '@plugin test', pluginAction)
  107. // Should return empty array instead of throwing error
  108. expect(result).toEqual([])
  109. })
  110. it('@app dedicated search should return empty array when API fails', async () => {
  111. // Mock app API failure
  112. mockFetchAppList.mockRejectedValue(new Error('App service unavailable'))
  113. const appAction = Actions.app
  114. const result = await searchAnything('en', '@app test', appAction)
  115. expect(result).toEqual([])
  116. })
  117. })
  118. describe('Error handling consistency validation', () => {
  119. it('all search types should return empty array when encountering errors', async () => {
  120. // Mock all APIs to fail
  121. mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
  122. mockFetchAppList.mockRejectedValue(new Error('App API failed'))
  123. mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed'))
  124. const actions = [
  125. { name: '@plugin', action: Actions.plugin },
  126. { name: '@app', action: Actions.app },
  127. { name: '@knowledge', action: Actions.knowledge },
  128. ]
  129. for (const { name, action } of actions) {
  130. const result = await action.search(name, 'test', 'en')
  131. expect(result).toEqual([])
  132. }
  133. })
  134. })
  135. describe('Edge case testing', () => {
  136. it('empty search term should be handled properly', async () => {
  137. mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } })
  138. const result = await searchAnything('en', '@plugin ', Actions.plugin)
  139. expect(result).toEqual([])
  140. })
  141. it('network timeout should be handled correctly', async () => {
  142. const timeoutError = new Error('Network timeout')
  143. timeoutError.name = 'TimeoutError'
  144. mockPostMarketplace.mockRejectedValue(timeoutError)
  145. const result = await searchAnything('en', '@plugin test', Actions.plugin)
  146. expect(result).toEqual([])
  147. })
  148. it('JSON parsing errors should be handled correctly', async () => {
  149. const parseError = new SyntaxError('Unexpected token in JSON')
  150. mockPostMarketplace.mockRejectedValue(parseError)
  151. const result = await searchAnything('en', '@plugin test', Actions.plugin)
  152. expect(result).toEqual([])
  153. })
  154. })
  155. })