// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import { UnknownError } from '@ledgerhq/coin-module-framework/errors' import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/live-network/errors' import { NetworkError, NotFoundError } from '@stellar/stellar-sdk' import { messageFromHorizonUnknown, throwHorizonLedgerOrOperationsError, } from '../horizonLedgerErrors' describe('messageFromHorizonUnknown', () => { it('returns Horizon documentationSummary when response body is present', () => { const error = Object.assign(new Error('Request failed with status code 400'), { response: { data: { title: 'Transaction Malformed', status: 400, detail: 'Horizon could not decode the transaction envelope in this request.', }, }, }) expect(messageFromHorizonUnknown(error)).toBe( 'Horizon could not decode the transaction envelope in this request.' ) }) it('returns message for Error', () => { expect(messageFromHorizonUnknown(new Error('hello'))).toBe('hello') }) it('returns string as-is', () => { expect(messageFromHorizonUnknown('plain')).toBe('plain') }) it('returns empty string for non-Error non-string', () => { expect(messageFromHorizonUnknown({ code: 1 })).toBe('') expect(messageFromHorizonUnknown(null)).toBe('') expect(messageFromHorizonUnknown(undefined)).toBe('') }) }) describe('throwHorizonLedgerOrOperationsError', () => { const notFound = 'ledger not found' it('throws notFoundMessage for NotFoundError', () => { expect(() => throwHorizonLedgerOrOperationsError(new NotFoundError('gone', {}), notFound) ).toThrow(notFound) }) it('throws notFoundMessage when message contains status code 404', () => { expect(() => throwHorizonLedgerOrOperationsError(new Error('Request failed: status code 404'), notFound) ).toThrow(notFound) }) it('throws notFoundMessage when Horizon problem body has status 404', () => { const error = Object.assign(new Error('Request failed with status code 404'), { response: { data: { title: 'Resource Missing', status: 404, detail: 'The resource at the url requested was not found.', }, }, }) expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(notFound) }) it('throws LedgerAPI4xx 429 for too many requests in message', () => { expect(() => throwHorizonLedgerOrOperationsError(new Error('Too Many Requests'), notFound) ).toThrow(LedgerAPI4xx) }) it('throws LedgerAPI4xx 429 when Horizon problem body has status 429', () => { const error = Object.assign(new Error('Request failed with status code 429'), { response: { data: { title: 'Rate Limited', status: 429, detail: 'The request was rate limited.', }, }, }) expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow( expect.objectContaining({ name: 'LedgerAPI4xx', status: 429 }) ) }) it('throws LedgerAPI4xx for other 4xx status in message', () => { expect(() => throwHorizonLedgerOrOperationsError(new Error('status code 400'), notFound) ).toThrow(LedgerAPI4xx) }) it('throws LedgerAPI4xx when Horizon problem body has status 400', () => { const error = Object.assign(new Error('Request failed with status code 400'), { response: { data: { title: 'Bad Request', status: 400, detail: 'The request you sent was invalid in some way.', }, }, }) expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(LedgerAPI4xx) }) test.each([['401'], ['403']])( 'throws LedgerAPI4xx when message contains status code %s', (code) => { expect(() => throwHorizonLedgerOrOperationsError( new Error(`Request failed: status code ${code}`), notFound ) ).toThrow(LedgerAPI4xx) } ) it('throws LedgerAPI5xx for 5xx status in message', () => { expect(() => throwHorizonLedgerOrOperationsError(new Error('status code 503'), notFound) ).toThrow(LedgerAPI5xx) }) it('throws LedgerAPI5xx when Horizon problem body has status 503', () => { const error = Object.assign(new Error('Request failed with status code 503'), { response: { data: { title: 'Internal Server Error', status: 503, detail: 'An error occurred while processing this request.', }, }, }) expect(() => throwHorizonLedgerOrOperationsError(error, notFound)).toThrow(LedgerAPI5xx) }) test.each([['502'], ['500']])( 'throws LedgerAPI5xx when message contains status code %s', (code) => { expect(() => throwHorizonLedgerOrOperationsError(new Error(`upstream status code ${code}`), notFound) ).toThrow(LedgerAPI5xx) } ) it('throws NetworkDown for NetworkError', () => { expect(() => throwHorizonLedgerOrOperationsError(new NetworkError('net', {}), notFound) ).toThrow(NetworkDown) }) test.each([ ['ECONNRESET', 'ECONNRESET on read'], ['ECONNREFUSED', 'connect ECONNREFUSED 127.0.0.1:443'], ['ENOTFOUND', 'getaddrinfo ENOTFOUND horizon.example'], ['EPIPE', 'write EPIPE broken pipe'], ['ETIMEDOUT', 'connect ETIMEDOUT 10.0.0.1:443'], ])('throws NetworkDown when message contains %s', (_label, message) => { expect(() => throwHorizonLedgerOrOperationsError(new Error(message), notFound)).toThrow( NetworkDown ) }) it('throws NetworkDown for undefined is not an object in message', () => { expect(() => throwHorizonLedgerOrOperationsError(new Error('undefined is not an object'), notFound) ).toThrow(NetworkDown) }) it('rethrows unknown errors with a cause', () => { const o = { custom: true } try { throwHorizonLedgerOrOperationsError(o, notFound) fail('Test should have thrown an error') } catch (e) { expect(e).toEqual(new UnknownError(undefined, o)) } }) it('should put the error message of the original error in the UnknownError', () => { const otherError = new Error('Error generated for the test') try { throwHorizonLedgerOrOperationsError(otherError, notFound) fail('Test should have thrown an error') } catch (error) { expect(error).toEqual(new UnknownError('Error generated for the test', otherError)) } }) })