AtAuth
1import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2import { HttpError, httpError, sanitizeError, internalError } from './errors.js';
3import type { ErrorResponse } from './errors.js';
4
5describe('HttpError', () => {
6 it('should store statusCode, code, and message', () => {
7 const err = new HttpError(400, 'bad_request', 'Invalid input');
8 expect(err.statusCode).toBe(400);
9 expect(err.code).toBe('bad_request');
10 expect(err.message).toBe('Invalid input');
11 expect(err.name).toBe('HttpError');
12 });
13
14 it('should be an instance of Error', () => {
15 const err = new HttpError(500, 'server_error', 'Boom');
16 expect(err).toBeInstanceOf(Error);
17 expect(err).toBeInstanceOf(HttpError);
18 });
19});
20
21describe('httpError factories', () => {
22 it('badRequest should create 400', () => {
23 const err = httpError.badRequest('missing_field', 'Field X is required');
24 expect(err.statusCode).toBe(400);
25 expect(err.code).toBe('missing_field');
26 expect(err.message).toBe('Field X is required');
27 });
28
29 it('unauthorized should create 401', () => {
30 const err = httpError.unauthorized('invalid_token', 'Token expired');
31 expect(err.statusCode).toBe(401);
32 expect(err.code).toBe('invalid_token');
33 });
34
35 it('forbidden should create 403', () => {
36 const err = httpError.forbidden('access_denied', 'Not allowed');
37 expect(err.statusCode).toBe(403);
38 expect(err.code).toBe('access_denied');
39 });
40
41 it('notFound should create 404', () => {
42 const err = httpError.notFound('not_found', 'Resource missing');
43 expect(err.statusCode).toBe(404);
44 expect(err.code).toBe('not_found');
45 });
46
47 it('conflict should create 409', () => {
48 const err = httpError.conflict('duplicate', 'Already exists');
49 expect(err.statusCode).toBe(409);
50 expect(err.code).toBe('duplicate');
51 });
52
53 it('internalServerError should create 500', () => {
54 const err = httpError.internalServerError('server_error', 'Something broke');
55 expect(err.statusCode).toBe(500);
56 expect(err.code).toBe('server_error');
57 });
58});
59
60describe('sanitizeError', () => {
61 let consoleSpy: ReturnType<typeof vi.spyOn>;
62
63 beforeEach(() => {
64 consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
65 });
66
67 afterEach(() => {
68 consoleSpy.mockRestore();
69 vi.unstubAllEnvs();
70 });
71
72 it('should log the error with context', () => {
73 const err = new Error('test error');
74 sanitizeError(err, 'Token verify');
75 expect(consoleSpy).toHaveBeenCalledWith('Token verify error:', err);
76 });
77
78 it('should return generic message in production', () => {
79 vi.stubEnv('NODE_ENV', 'production');
80 const result = sanitizeError(new Error('secret details'), 'ctx');
81 expect(result).toBe('An internal error occurred. Please try again later.');
82 });
83
84 it('should return generic message in test mode', () => {
85 // NODE_ENV is 'test' by default in vitest
86 const result = sanitizeError(new Error('details'), 'ctx');
87 expect(result).toBe('An internal error occurred. Please try again later.');
88 });
89
90 it('should strip file paths in development mode', () => {
91 vi.stubEnv('NODE_ENV', 'development');
92 const result = sanitizeError(new Error('Failed at /home/user/app/src/index.ts:42'), 'ctx');
93 expect(result).not.toContain('/home/user');
94 expect(result).toContain('[path]');
95 });
96
97 it('should return generic message for non-Error objects', () => {
98 vi.stubEnv('NODE_ENV', 'development');
99 const result = sanitizeError('string error', 'ctx');
100 expect(result).toBe('An internal error occurred. Please try again later.');
101 });
102});
103
104describe('internalError', () => {
105 let consoleSpy: ReturnType<typeof vi.spyOn>;
106
107 beforeEach(() => {
108 consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
109 });
110
111 afterEach(() => {
112 consoleSpy.mockRestore();
113 });
114
115 it('should return ErrorResponse with error code and sanitized message', () => {
116 const result: ErrorResponse = internalError('db_error', new Error('connection lost'), 'DB query');
117 expect(result.error).toBe('db_error');
118 expect(result.message).toBeTypeOf('string');
119 expect(result.message).not.toContain('connection lost');
120 });
121});