@eleva-io/erp-sdk
Version:
SDK oficial para el ERP de Eleva
247 lines • 12.7 kB
JavaScript
import { z } from 'zod';
import { ElevaError, ERROR_CODES, ErrorKind } from '.';
const validationDetails = {
errors: [{ validation: 'email', code: 'invalid_string', message: 'Invalid email', path: ['email'] }],
};
const notFoundDetails = { entity: 'User' };
const conflictDetails = [{ key: 'email', value: 'already@taken.com', cause: 'duplicate' }];
describe('ElevaError', () => {
describe('constructor', () => {
it('is an instance of Error and ElevaError', () => {
const error = ElevaError.internal({});
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(ElevaError);
});
it('sets name to "ElevaError"', () => {
const error = ElevaError.internal({});
expect(error.name).toBe('ElevaError');
});
it('picks kind, status and default message from the catalog', () => {
const error = ElevaError.internal({});
expect(error.code).toBe(ERROR_CODES.INTERNAL_SERVER_ERROR);
expect(error.kind).toBe(ErrorKind.SYSTEM);
expect(error.status).toBe(500);
expect(error.message).toBe('Internal server error');
});
it('uses a custom message when provided', () => {
const error = ElevaError.internal({ message: 'Something blew up' });
expect(error.message).toBe('Something blew up');
});
it('exposes structured details (VALIDATION)', () => {
const error = ElevaError.validation(validationDetails);
expect(error.details).toEqual(validationDetails);
});
it('exposes structured details (NOT_FOUND)', () => {
const error = ElevaError.notFound(notFoundDetails);
expect(error.details).toEqual(notFoundDetails);
});
it('exposes structured details (CONFLICT)', () => {
const error = ElevaError.conflict({ details: conflictDetails });
expect(error.details).toEqual(conflictDetails);
});
it('allows omitting details for codes with fully optional shapes', () => {
const error = ElevaError.unauthorized({});
expect(error.code).toBe(ERROR_CODES.UNAUTHORIZED);
expect(error.details).toEqual({});
});
it('forwards cause to the underlying Error', () => {
const cause = new Error('boom');
const error = ElevaError.internal({ cause });
expect(error.cause).toBe(cause);
});
it('captures a stack trace', () => {
const error = ElevaError.internal({});
expect(error.stack).toMatch(/ElevaError/);
});
});
describe('is()', () => {
it('returns true when the code matches', () => {
const error = ElevaError.notFound(notFoundDetails);
expect(error.is(ERROR_CODES.NOT_FOUND)).toBe(true);
});
it('returns false when the code does not match', () => {
const error = ElevaError.notFound(notFoundDetails);
expect(error.is(ERROR_CODES.CONFLICT)).toBe(false);
});
it('narrows details to the matching code', () => {
const err = ElevaError.notFound(notFoundDetails);
const entity = err.is(ERROR_CODES.NOT_FOUND) ? err.details.entity : null;
expect(entity).toBe('User');
});
it('returns true when a plain string matches the code', () => {
const error = ElevaError.notFound(notFoundDetails);
expect(error.is('NOT_FOUND')).toBe(true);
});
it('returns false when a plain string does not match the code', () => {
const error = ElevaError.notFound(notFoundDetails);
expect(error.is('CONFLICT')).toBe(false);
});
it('returns true for uncatalogued codes matched by string', () => {
const error = ElevaError.parse({ code: 'CUSTOM_CODE', message: 'custom' });
expect(error.is('CUSTOM_CODE')).toBe(true);
});
});
describe('toJSON() / toString()', () => {
it('toJSON returns a plain object with all fields', () => {
const error = ElevaError.validation(validationDetails);
expect(error.toJSON()).toEqual({
code: ERROR_CODES.VALIDATION,
kind: ErrorKind.DOMAIN,
status: 400,
message: 'Validation error',
details: validationDetails,
});
});
it('toString returns a JSON string equivalent to toJSON()', () => {
const error = ElevaError.validation(validationDetails);
expect(error.toString()).toBe(JSON.stringify(error.toJSON()));
});
});
describe('parse()', () => {
it('reconstructs an ElevaError from a valid response body', () => {
const body = {
code: ERROR_CODES.VALIDATION,
message: 'Validation error',
details: validationDetails,
};
const error = ElevaError.parse(body);
expect(error).toBeInstanceOf(ElevaError);
expect(error.code).toBe(ERROR_CODES.VALIDATION);
expect(error.message).toBe('Validation error');
expect(error.details).toEqual(validationDetails);
});
it('derives kind and status from the catalog when not provided', () => {
const error = ElevaError.parse({ code: ERROR_CODES.NOT_FOUND, message: 'Not found' });
expect(error.kind).toBe(ErrorKind.DOMAIN);
expect(error.status).toBe(404);
});
it('uses kind and status from the payload when provided', () => {
const body = { code: ERROR_CODES.NOT_FOUND, kind: ErrorKind.SYSTEM, status: 503, message: 'Override' };
const error = ElevaError.parse(body);
expect(error.kind).toBe(ErrorKind.SYSTEM);
expect(error.status).toBe(503);
});
it('falls back to catalog message when message is not provided', () => {
const error = ElevaError.parse({ code: ERROR_CODES.NOT_FOUND });
expect(error.message).toBe('Not found');
});
it('preserves uncatalogued codes and their message as-is', () => {
const body = { code: 'SOME_NEW_CODE', message: 'Something went wrong on the server' };
const error = ElevaError.parse(body);
expect(error.code).toBe('SOME_NEW_CODE');
expect(error.message).toBe('Something went wrong on the server');
});
it('falls back to INTERNAL_SERVER_ERROR kind and status for uncatalogued codes', () => {
const body = { code: 'SOME_NEW_CODE', message: 'New error type' };
const error = ElevaError.parse(body);
expect(error.kind).toBe(ErrorKind.SYSTEM);
expect(error.status).toBe(500);
});
it('falls back to INTERNAL_SERVER_ERROR on malformed payloads', () => {
const body = 'not an object';
const error = ElevaError.parse(body);
expect(error.code).toBe(ERROR_CODES.INTERNAL_SERVER_ERROR);
expect(error.message).toMatch(/^Failed to parse error payload:/);
expect(error.cause).toBe(body);
});
});
describe('fromZodError()', () => {
const fail = (schema, value) => {
const result = schema.safeParse(value);
return result.success ? new z.ZodError([]) : result.error;
};
it('converts a ZodError into a VALIDATION ElevaError', () => {
const schema = z.object({ email: z.string().email(), age: z.number().int() });
const error = ElevaError.fromZodError(fail(schema, { email: 'not-an-email', age: 1.5 }));
expect(error.code).toBe(ERROR_CODES.VALIDATION);
expect(error.details.errors.length).toBeGreaterThan(0);
const first = error.details.errors[0];
expect(first.path).toEqual(expect.any(Array));
expect(typeof first.message).toBe('string');
});
it('uses "root" as path label when issue path is empty', () => {
const error = ElevaError.fromZodError(fail(z.string(), 123));
expect(error.details.errors[0].validation).toBe('root');
});
});
describe('factories', () => {
it('validation() accepts an errors array', () => {
const error = ElevaError.validation({
errors: [
{ validation: 'x', code: 'c', message: 'm', path: ['x'] },
{ validation: 'y', code: 'c', message: 'm', path: ['y'] },
],
});
expect(error.code).toBe(ERROR_CODES.VALIDATION);
expect(error.details.errors).toHaveLength(2);
});
it('invalidField() builds a single-field validation error', () => {
const error = ElevaError.invalidField({
error: { validation: 'email', code: 'invalid_string', message: 'Invalid email', path: ['email'] },
});
expect(error.code).toBe(ERROR_CODES.VALIDATION);
expect(error.details.errors[0]).toEqual({
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: ['email'],
});
});
it('notFound() builds a NOT_FOUND error with entity', () => {
const error = ElevaError.notFound({ entity: 'User' });
expect(error.code).toBe(ERROR_CODES.NOT_FOUND);
expect(error.details).toEqual({ entity: 'User' });
});
it('conflict() builds a CONFLICT error', () => {
const error = ElevaError.conflict({ details: conflictDetails });
expect(error.code).toBe(ERROR_CODES.CONFLICT);
expect(error.details).toEqual(conflictDetails);
});
it('simple factories use catalog defaults with no args', () => {
expect(ElevaError.badRequest().code).toBe(ERROR_CODES.BAD_REQUEST);
expect(ElevaError.authentication().code).toBe(ERROR_CODES.AUTHENTICATION);
expect(ElevaError.unauthorized().code).toBe(ERROR_CODES.UNAUTHORIZED);
expect(ElevaError.notAcceptable().code).toBe(ERROR_CODES.NOT_ACCEPTABLE);
expect(ElevaError.gone().code).toBe(ERROR_CODES.GONE);
expect(ElevaError.unprocessableEntity().code).toBe(ERROR_CODES.UNPROCESSABLE_ENTITY);
expect(ElevaError.serviceUnavailable().code).toBe(ERROR_CODES.SERVICE_UNAVAILABLE);
});
it('unprocessableEntity() carries optional structured details', () => {
const error = ElevaError.unprocessableEntity({ details: { field: 'email', reason: 'already_taken' } });
expect(error.code).toBe(ERROR_CODES.UNPROCESSABLE_ENTITY);
expect(error.status).toBe(422);
expect(error.details).toEqual({ field: 'email', reason: 'already_taken' });
});
it('simple factories accept message and details', () => {
const error = ElevaError.unauthorized({
message: 'Forbidden',
details: { requiredScope: 'admin', resource: 'users' },
});
expect(error.message).toBe('Forbidden');
expect(error.details).toEqual({ requiredScope: 'admin', resource: 'users' });
});
it('badRequest() carries optional structured details', () => {
const error = ElevaError.badRequest({ details: { field: 'id', reason: 'required' } });
expect(error.code).toBe(ERROR_CODES.BAD_REQUEST);
expect(error.details).toEqual({ field: 'id', reason: 'required' });
});
it('mandatoryField() builds a MANDATORY_FIELD error with field', () => {
const error = ElevaError.mandatoryField({ field: 'agentId' });
expect(error.code).toBe(ERROR_CODES.MANDATORY_FIELD);
expect(error.status).toBe(400);
expect(error.details).toEqual({ field: 'agentId' });
});
it('mandatoryField() accepts a custom message', () => {
const error = ElevaError.mandatoryField({ field: 'permissions', message: 'permissions is required' });
expect(error.message).toBe('permissions is required');
expect(error.details).toEqual({ field: 'permissions' });
});
it('internal() propagates the cause', () => {
const cause = new Error('db down');
const error = ElevaError.internal({ cause });
expect(error.code).toBe(ERROR_CODES.INTERNAL_SERVER_ERROR);
expect(error.cause).toBe(cause);
});
});
});
//# sourceMappingURL=errors.spec.js.map