error-reporting.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import * as Sentry from '@sentry/nextjs'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { captureCriticalError } from './error-reporting'
  4. import { ResponseError } from '@/types'
  5. vi.mock('@sentry/nextjs', () => ({
  6. captureException: vi.fn(),
  7. withScope: vi.fn((cb: (scope: any) => void) => {
  8. const scope = { setTag: vi.fn() }
  9. cb(scope)
  10. }),
  11. }))
  12. describe('error-reporting', () => {
  13. beforeEach(() => {
  14. vi.clearAllMocks()
  15. })
  16. describe('captureCriticalError', () => {
  17. it('should not capture error if message is empty', () => {
  18. const error = { message: '' }
  19. captureCriticalError(error, 'test context')
  20. expect(Sentry.captureException).not.toHaveBeenCalled()
  21. })
  22. it('should capture regular Error objects', () => {
  23. const error = new Error('Something went wrong')
  24. captureCriticalError(error, 'test action')
  25. expect(Sentry.captureException).toHaveBeenCalledWith(
  26. expect.objectContaining({ message: 'Something went wrong', name: 'CriticalError' })
  27. )
  28. })
  29. it('should not capture whitelisted error messages', () => {
  30. const error = new Error('email must be an email')
  31. captureCriticalError(error, 'validation')
  32. expect(Sentry.captureException).not.toHaveBeenCalled()
  33. })
  34. it('should not capture errors with partial whitelisted message', () => {
  35. const error = new Error('User error: A user with this email already exists in the system')
  36. captureCriticalError(error, 'sign up')
  37. expect(Sentry.captureException).not.toHaveBeenCalled()
  38. })
  39. it('should capture errors that are not whitelisted', () => {
  40. const error = new Error('Database connection failed')
  41. captureCriticalError(error, 'database')
  42. expect(Sentry.captureException).toHaveBeenCalledWith(
  43. expect.objectContaining({ message: 'Database connection failed', name: 'CriticalError' })
  44. )
  45. })
  46. it('should capture 5XX ResponseError', () => {
  47. const error = new ResponseError(
  48. 'Internal server error',
  49. 500,
  50. undefined,
  51. undefined,
  52. '/api/test'
  53. )
  54. captureCriticalError(error, 'api call')
  55. expect(Sentry.captureException).toHaveBeenCalledWith(
  56. expect.objectContaining({
  57. message: 'requestPathname /api/test w/ message: Internal server error',
  58. name: 'CriticalError',
  59. })
  60. )
  61. })
  62. it('should not capture 4XX ResponseError', () => {
  63. const error = new ResponseError('Not found', 404, undefined, undefined, '/api/test')
  64. captureCriticalError(error, 'api call')
  65. expect(Sentry.captureException).not.toHaveBeenCalled()
  66. })
  67. it('should capture ResponseError with 5XX status code', () => {
  68. const error = new ResponseError('Gateway timeout', 504, undefined, undefined, '/api/gateway')
  69. captureCriticalError(error, 'gateway request')
  70. expect(Sentry.captureException).toHaveBeenCalledWith(
  71. expect.objectContaining({
  72. message: 'requestPathname /api/gateway w/ message: Gateway timeout',
  73. name: 'CriticalError',
  74. })
  75. )
  76. })
  77. it('should capture ResponseError without code or requestPathname', () => {
  78. const error = new ResponseError('Unknown error')
  79. captureCriticalError(error, 'unknown')
  80. expect(Sentry.captureException).toHaveBeenCalledWith(
  81. expect.objectContaining({
  82. message: 'Response Error (no code or requestPathname) w/ message: Unknown error',
  83. name: 'CriticalError',
  84. })
  85. )
  86. })
  87. it('should capture unknown error objects with message property', () => {
  88. const error = { message: 'Custom error object' }
  89. captureCriticalError(error, 'custom')
  90. expect(Sentry.captureException).toHaveBeenCalledWith(
  91. expect.objectContaining({ message: 'Custom error object', name: 'CriticalError' })
  92. )
  93. })
  94. it('should not capture unknown error without message', () => {
  95. const error = { foo: 'bar' }
  96. captureCriticalError(error as any, 'no message')
  97. expect(Sentry.captureException).not.toHaveBeenCalled()
  98. })
  99. it('should not capture whitelisted password validation error', () => {
  100. const error = new Error(
  101. 'Password is known to be weak and easy to guess, please choose a different one'
  102. )
  103. captureCriticalError(error, 'password update')
  104. expect(Sentry.captureException).not.toHaveBeenCalled()
  105. })
  106. it('should not capture whitelisted TOTP error', () => {
  107. const error = new Error('Invalid TOTP code entered')
  108. captureCriticalError(error, 'mfa verification')
  109. expect(Sentry.captureException).not.toHaveBeenCalled()
  110. })
  111. it('should not capture whitelisted project name error', () => {
  112. const error = new Error('name should not contain a . string')
  113. captureCriticalError(error, 'create project')
  114. expect(Sentry.captureException).not.toHaveBeenCalled()
  115. })
  116. it('should capture non-whitelisted errors even if similar to whitelisted ones', () => {
  117. const error = new Error('email format is invalid')
  118. captureCriticalError(error, 'validation')
  119. expect(Sentry.captureException).toHaveBeenCalledWith(
  120. expect.objectContaining({ message: 'email format is invalid', name: 'CriticalError' })
  121. )
  122. })
  123. it('should handle Error objects with empty message', () => {
  124. const error = new Error('')
  125. captureCriticalError(error, 'empty error')
  126. expect(Sentry.captureException).not.toHaveBeenCalled()
  127. })
  128. it('should handle ResponseError at boundary of 4XX/5XX (499)', () => {
  129. const error = new ResponseError(
  130. 'Client closed request',
  131. 499,
  132. undefined,
  133. undefined,
  134. '/api/test'
  135. )
  136. captureCriticalError(error, 'request')
  137. expect(Sentry.captureException).not.toHaveBeenCalled()
  138. })
  139. it('should handle ResponseError at boundary of 4XX/5XX (500)', () => {
  140. const error = new ResponseError('Internal error', 500, undefined, undefined, '/api/test')
  141. captureCriticalError(error, 'request')
  142. expect(Sentry.captureException).toHaveBeenCalledWith(
  143. expect.objectContaining({
  144. message: 'requestPathname /api/test w/ message: Internal error',
  145. name: 'CriticalError',
  146. })
  147. )
  148. })
  149. })
  150. })