import { BotError, ErrorCategory } from '../../src/errors/BotError'; import { ErrorCodes, createBotError } from '../../src/errors/ErrorCodes'; import { ErrorReporter } from '../../src/errors/ErrorReporter'; // Mock logger to prevent console output during tests jest.mock('../../src/utils/logger', () => ({ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), }, })); describe('ErrorReporter', () => { describe('wrap()', () => { it('should return BotError as-is if already a BotError', () => { const original = createBotError(ErrorCodes.INVALID_USER_LIMIT); const wrapped = ErrorReporter.wrap(original); expect(wrapped).toBe(original); expect(wrapped.code).toBe('E1001'); }); it('should map Discord error code 50013 to DISCORD_MISSING_PERMISSIONS', () => { const discordError = new Error('Missing Permissions') as any; discordError.code = 50013; const wrapped = ErrorReporter.wrap(discordError); expect(wrapped).toBeInstanceOf(BotError); expect(wrapped.code).toBe('E4002'); expect(wrapped.category).toBe(ErrorCategory.DISCORD_API); expect(wrapped.cause).toBe(discordError); }); it('should map Discord error code 50001 to BOT_MISSING_MANAGE_CHANNELS', () => { const discordError = new Error('Missing Access') as any; discordError.code = 50001; const wrapped = ErrorReporter.wrap(discordError); expect(wrapped).toBeInstanceOf(BotError); expect(wrapped.code).toBe('E2001'); expect(wrapped.category).toBe(ErrorCategory.PERMISSION); }); it('should map HTTP 429 to DISCORD_RATE_LIMITED', () => { const rateLimitError = new Error('Rate Limited') as any; rateLimitError.httpStatus = 429; const wrapped = ErrorReporter.wrap(rateLimitError); expect(wrapped).toBeInstanceOf(BotError); expect(wrapped.code).toBe('E4001'); expect(wrapped.category).toBe(ErrorCategory.DISCORD_API); }); it('should wrap unknown errors as UNKNOWN_ERROR', () => { const unknownError = new Error('Something unexpected'); const wrapped = ErrorReporter.wrap(unknownError); expect(wrapped).toBeInstanceOf(BotError); expect(wrapped.code).toBe('E3999'); expect(wrapped.category).toBe(ErrorCategory.BOT_INTERNAL); expect(wrapped.cause).toBe(unknownError); }); it('should handle non-Error objects (e.g. strings)', () => { const wrapped = ErrorReporter.wrap('string error'); expect(wrapped).toBeInstanceOf(BotError); expect(wrapped.code).toBe('E3999'); }); }); describe('report()', () => { it('should reply with an ephemeral embed when interaction has not been replied', async () => { const mockInteraction = { replied: false, deferred: false, reply: jest.fn().mockResolvedValue(undefined), followUp: jest.fn().mockResolvedValue(undefined), } as any; const error = createBotError(ErrorCodes.NOT_CHANNEL_OWNER); await ErrorReporter.report(mockInteraction, error, 'en'); expect(mockInteraction.reply).toHaveBeenCalledTimes(1); expect(mockInteraction.followUp).not.toHaveBeenCalled(); const replyArgs = mockInteraction.reply.mock.calls[0][0]; expect(replyArgs.ephemeral).toBe(true); expect(replyArgs.embeds).toHaveLength(1); // Verify embed does NOT contain error code const embed = replyArgs.embeds[0]; const embedJSON = embed.toJSON(); expect(embedJSON.description).not.toContain('E2004'); expect(embedJSON.footer).toBeUndefined(); }); it('should followUp when interaction has already been replied', async () => { const mockInteraction = { replied: true, deferred: false, reply: jest.fn().mockResolvedValue(undefined), followUp: jest.fn().mockResolvedValue(undefined), } as any; const error = createBotError(ErrorCodes.DATABASE_ERROR); await ErrorReporter.report(mockInteraction, error, 'en'); expect(mockInteraction.followUp).toHaveBeenCalledTimes(1); expect(mockInteraction.reply).not.toHaveBeenCalled(); }); it('should include localized resolution field in embed when available', async () => { const mockInteraction = { replied: false, deferred: false, reply: jest.fn().mockResolvedValue(undefined), } as any; const error = createBotError(ErrorCodes.INVALID_USER_LIMIT); await ErrorReporter.report(mockInteraction, error, 'en'); const embed = mockInteraction.reply.mock.calls[0][0].embeds[0]; const embedJSON = embed.toJSON(); expect(embedJSON.fields).toHaveLength(1); expect(embedJSON.fields[0].name).toBe('πŸ’‘ How to resolve'); }); it('should render Korean messages when locale is ko', async () => { const mockInteraction = { replied: false, deferred: false, reply: jest.fn().mockResolvedValue(undefined), } as any; const error = createBotError(ErrorCodes.INVALID_USER_LIMIT); await ErrorReporter.report(mockInteraction, error, 'ko'); const embed = mockInteraction.reply.mock.calls[0][0].embeds[0]; const embedJSON = embed.toJSON(); expect(embedJSON.title).toContain('μž…λ ₯을 ν™•μΈν•΄μ£Όμ„Έμš”'); expect(embedJSON.description).toBe('μ‚¬μš©μž μ œν•œ 값이 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'); expect(embedJSON.fields[0].name).toBe('πŸ’‘ ν•΄κ²° 방법'); }); it('should not throw even if reply fails', async () => { const mockInteraction = { replied: false, deferred: false, reply: jest.fn().mockRejectedValue(new Error('Network error')), } as any; const error = createBotError(ErrorCodes.UNKNOWN_ERROR); // Should not throw await expect(ErrorReporter.report(mockInteraction, error, 'en')).resolves.not.toThrow(); }); }); }); describe('createBotError()', () => { it('should create a fresh BotError instance from a definition', () => { const error1 = createBotError(ErrorCodes.DATABASE_ERROR); const error2 = createBotError(ErrorCodes.DATABASE_ERROR); expect(error1).not.toBe(error2); expect(error1.code).toBe(error2.code); expect(error1.messageKey).toBe(error2.messageKey); }); it('should attach a cause error', () => { const cause = new Error('Connection refused'); const error = createBotError(ErrorCodes.DATABASE_ERROR, cause); expect(error.cause).toBe(cause); }); });