169 lines
5.7 KiB
TypeScript
169 lines
5.7 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);
|
|
|
|
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);
|
|
|
|
expect(mockInteraction.followUp).toHaveBeenCalledTimes(1);
|
|
expect(mockInteraction.reply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should include 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);
|
|
|
|
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 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)).resolves.not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createBotError()', () => {
|
|
it('should create a fresh BotError instance from a template', () => {
|
|
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.userMessage).toBe(error2.userMessage);
|
|
});
|
|
|
|
it('should attach a cause error', () => {
|
|
const cause = new Error('Connection refused');
|
|
const error = createBotError(ErrorCodes.DATABASE_ERROR, cause);
|
|
|
|
expect(error.cause).toBe(cause);
|
|
});
|
|
});
|