@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
185 lines (163 loc) • 6.01 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import { LedgerAPI4xx, LedgerAPI5xx, NetworkDown } from '@ledgerhq/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', () => {
const o = { custom: true }
try {
throwHorizonLedgerOrOperationsError(o, notFound)
} catch (e) {
expect(e).toBe(o)
return
}
throw new Error('expected throw')
})
})