// SPDX-FileCopyrightText: © 2024 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CurrencyConfig, Context } from '../../config' import type { CoinModuleImpl } from '../impl' import type { BlockInfo, ListOperationsOptions, TransactionIntent } from '../types' import type { Tracer, Span } from './withTracing' import { withDefaults } from '../withDefaults' import { withLogging } from './withLogging' import { withTracing } from './withTracing' type Cfg = CurrencyConfig type Ctx = Context const block: BlockInfo = { height: 1, hash: '0xabc', time: new Date(0) } const ctx: Ctx = { config: () => Promise.reject(new Error('stub')), logger: () => undefined, } const minimalModule: CoinModuleImpl = { lastBlock: (_ctx: Ctx): Promise => Promise.reject(new Error('stub')), getBalance: (_ctx: Ctx, _addr: string) => Promise.resolve([]), listOperations: (_ctx: Ctx, _addr: string, _opts: ListOperationsOptions) => Promise.resolve({ items: [] }), craftTransaction: (_ctx: Ctx, _intent: TransactionIntent) => Promise.reject(new Error('stub')), estimateFees: (_ctx: Ctx, _intent: TransactionIntent) => Promise.reject(new Error('stub')), combine: (_ctx: Ctx, tx: string, _sig: string[]): string => tx, broadcast: (_ctx: Ctx, _tx: string) => Promise.reject(new Error('stub')), craftTransactionData: (_ctx: Ctx, _intent: TransactionIntent) => ({ type: 'none' }) as const, } function makeSpan(): { mock: Span; end: jest.Mock; setError: jest.Mock } { const end = jest.fn() const setError = jest.fn() return { mock: { end, setError }, end, setError } } function makeTracer(span: Span): { mock: Tracer; startSpan: jest.Mock } { const startSpan = jest.fn().mockReturnValue(span) return { mock: { startSpan }, startSpan } } describe('withTracing', () => { it('calls startSpan with the method name', async () => { const { mock: span } = makeSpan() const { mock: tracer, startSpan } = makeTracer(span) const impl: CoinModuleImpl = { ...minimalModule, lastBlock: (_c: Ctx) => Promise.resolve(block), } await withTracing(withDefaults(impl), tracer).lastBlock(ctx) expect(startSpan).toHaveBeenCalledWith('lastBlock') }) it('calls span.end() after a successful async method', async () => { const { mock: span, end } = makeSpan() const { mock: tracer } = makeTracer(span) const impl: CoinModuleImpl = { ...minimalModule, lastBlock: (_c: Ctx) => Promise.resolve(block), } await withTracing(withDefaults(impl), tracer).lastBlock(ctx) expect(end).toHaveBeenCalledTimes(1) }) it('calls span.setError and span.end then re-throws on a rejected async method', async () => { const error = new Error('net-fail') const { mock: span, end, setError } = makeSpan() const { mock: tracer } = makeTracer(span) const impl: CoinModuleImpl = { ...minimalModule, lastBlock: (_c: Ctx) => Promise.reject(error), } await expect(withTracing(withDefaults(impl), tracer).lastBlock(ctx)).rejects.toThrow(error) expect(setError).toHaveBeenCalledWith(error) expect(end).toHaveBeenCalledTimes(1) }) it('calls span.end and returns result for a successful synchronous method', () => { const { mock: span, end } = makeSpan() const { mock: tracer } = makeTracer(span) const result = withTracing(withDefaults(minimalModule), tracer).combine(ctx, 'tx', []) expect(result).toBe('tx') expect(end).toHaveBeenCalledTimes(1) }) it('calls span.setError and span.end then re-throws on a synchronous throw', () => { const error = new Error('sync-fail') const { mock: span, end, setError } = makeSpan() const { mock: tracer } = makeTracer(span) const impl: CoinModuleImpl = { ...minimalModule, combine: (_c: Ctx, _tx: string, _sig: string[]): string => { throw error }, } expect(() => withTracing(withDefaults(impl), tracer).combine(ctx, 'tx', [])).toThrow(error) expect(setError).toHaveBeenCalledWith(error) expect(end).toHaveBeenCalledTimes(1) }) it('stacks logging and tracing when composed withLogging(withTracing(api, tracer))', async () => { const logger = jest.fn() const logCtx: Ctx = { ...ctx, logger } const { mock: span, end } = makeSpan() const { mock: tracer, startSpan } = makeTracer(span) const impl: CoinModuleImpl = { ...minimalModule, lastBlock: (_c: Ctx) => Promise.resolve(block), } const composed = withLogging(withTracing(withDefaults(impl), tracer)) await composed.lastBlock(logCtx) // Logging layer fired expect(logger).toHaveBeenCalledWith('[coin-module] lastBlock: call') expect(logger).toHaveBeenCalledWith('[coin-module] lastBlock: ok') // Tracing layer fired expect(startSpan).toHaveBeenCalledWith('lastBlock') expect(end).toHaveBeenCalledTimes(1) // The underlying method's return value is preserved through both layers const result = await composed.lastBlock(logCtx) expect(result).not.toBe(undefined) }) })