Kord/apps/bot/tests/errors/ErrorReporter.test.ts

187 lines
6.4 KiB
TypeScript

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);
});
});