Skip to content

Commit 745220e

Browse files
committed
test(sep41): cover the per-process metadata caches
The fetched/failedUntil caches sit on the production ingestion path but had no coverage: nothing verified that a resolved contract stops hitting the RPC, that a failed fetch is suppressed for the backoff window, or that expiry re-fetches and success clears the failure entry. Add those tests; shortening metadataFailureBackoff here is what makes its var-for-tests comment true.
1 parent a46bacd commit 745220e

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

internal/services/sep41/metadata_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,94 @@ func TestMetadataFetcher_FetchMetadata(t *testing.T) {
217217
assert.Empty(t, out)
218218
})
219219

220+
t.Run("skips a contract that is still inside its failure-backoff window", func(t *testing.T) {
221+
rpc := services.NewContractMetadataServiceMock(t)
222+
// One failing name() simulation is all the RPC is allowed to see: the
223+
// backoff entry recorded by the first call must suppress the second.
224+
rpc.On("FetchSingleField", mock.Anything, testContractA, "name", mock.Anything).
225+
Return(xdr.ScVal{}, errors.New("simulate boom")).Once()
226+
227+
f := newMetadataFetcher(rpc, pond.NewPool(2))
228+
out, err := f.FetchMetadata(ctx, []string{testContractA})
229+
require.NoError(t, err)
230+
assert.Empty(t, out)
231+
232+
out, err = f.FetchMetadata(ctx, []string{testContractA})
233+
require.NoError(t, err)
234+
assert.Empty(t, out)
235+
rpc.AssertNumberOfCalls(t, "FetchSingleField", 1)
236+
})
237+
238+
t.Run("refetches a contract once its failure-backoff window expires", func(t *testing.T) {
239+
// The deadline in failedUntil is absolute, so the backoff has to be short
240+
// before the failure is recorded — shortening it afterwards would leave the
241+
// original five-minute deadline in place.
242+
orig := metadataFailureBackoff
243+
metadataFailureBackoff = 20 * time.Millisecond
244+
t.Cleanup(func() { metadataFailureBackoff = orig })
245+
246+
rpc := services.NewContractMetadataServiceMock(t)
247+
rpc.On("FetchSingleField", mock.Anything, testContractA, "name", mock.Anything).
248+
Return(xdr.ScVal{}, errors.New("simulate boom")).Once()
249+
expectMetadataFetch(rpc, testContractA, "USD Coin", "USDC", 7)
250+
251+
f := newMetadataFetcher(rpc, pond.NewPool(2))
252+
out, err := f.FetchMetadata(ctx, []string{testContractA})
253+
require.NoError(t, err)
254+
assert.Empty(t, out)
255+
256+
// Calls inside the window are served from the failure cache and return
257+
// nothing; the first call past the deadline reaches the (now healthy) RPC.
258+
require.Eventually(t, func() bool {
259+
out, err := f.FetchMetadata(ctx, []string{testContractA})
260+
return err == nil && len(out) == 1
261+
}, time.Second, 5*time.Millisecond)
262+
263+
// The successful fetch cleared the failure entry and marked the contract
264+
// fetched, so it never reaches the RPC again.
265+
out, err = f.FetchMetadata(ctx, []string{testContractA})
266+
require.NoError(t, err)
267+
assert.Empty(t, out)
268+
rpc.AssertNumberOfCalls(t, "FetchSingleField", 4) // 1 failed name + name/symbol/decimals
269+
})
270+
271+
t.Run("fetches a contract only once per process", func(t *testing.T) {
272+
rpc := services.NewContractMetadataServiceMock(t)
273+
expectMetadataFetch(rpc, testContractA, "USD Coin", "USDC", 7)
274+
275+
f := newMetadataFetcher(rpc, pond.NewPool(2))
276+
out, err := f.FetchMetadata(ctx, []string{testContractA})
277+
require.NoError(t, err)
278+
require.Len(t, out, 1)
279+
280+
// Already-fetched contracts are skipped entirely: Apply has persisted their
281+
// metadata, so the second call returns nothing rather than refetching.
282+
out, err = f.FetchMetadata(ctx, []string{testContractA})
283+
require.NoError(t, err)
284+
assert.Empty(t, out)
285+
rpc.AssertNumberOfCalls(t, "FetchSingleField", 3)
286+
})
287+
288+
t.Run("fetches only the uncached contract of a mixed batch", func(t *testing.T) {
289+
rpc := services.NewContractMetadataServiceMock(t)
290+
expectMetadataFetch(rpc, testContractA, "USD Coin", "USDC", 7)
291+
expectMetadataFetch(rpc, testContractB, "Euro Coin", "EURC", 6)
292+
293+
f := newMetadataFetcher(rpc, pond.NewPool(2))
294+
out, err := f.FetchMetadata(ctx, []string{testContractA})
295+
require.NoError(t, err)
296+
require.Len(t, out, 1)
297+
298+
out, err = f.FetchMetadata(ctx, []string{testContractA, testContractB})
299+
require.NoError(t, err)
300+
require.Len(t, out, 1)
301+
assert.NotContains(t, out, testContractA)
302+
require.NotNil(t, out[testContractB])
303+
require.NotNil(t, out[testContractB].Symbol)
304+
assert.Equal(t, "EURC", *out[testContractB].Symbol)
305+
rpc.AssertNumberOfCalls(t, "FetchSingleField", 6)
306+
})
307+
220308
t.Run("processes contracts in multiple batches when input exceeds the batch size", func(t *testing.T) {
221309
// Shrink batch knobs so we exercise the multi-batch loop without sleeping
222310
// for real seconds.

0 commit comments

Comments
 (0)