@@ -15,6 +15,9 @@ interface FakeAccount {
1515interface RawOrder {
1616 orderId : string
1717 isoDate : string
18+ // Only set by tests exercising the skip handler's cursor advance, which
19+ // reads createdAt from the raw order the way the live API shapes it.
20+ createdAt ?: string
1821}
1922interface Captured {
2023 affiliateId : string
@@ -129,6 +132,27 @@ describe('getSideshiftAccounts', function() {
129132 { affiliateId : 'SAME' , affiliateSecret : 'sprimary' }
130133 ] )
131134 } )
135+
136+ it ( 'throws when only one of the added-account fields is configured' , function ( ) {
137+ // A half-configured pair means the operator intended a second account.
138+ // Silently querying only the primary would hide the misconfiguration.
139+ expect ( ( ) =>
140+ getSideshiftAccounts ( {
141+ sideshiftAffiliateId : 'PRIMARY' ,
142+ sideshiftAffiliateSecret : 'sprimary' ,
143+ sideshiftAffiliateIdNew : 'ADDED' ,
144+ sideshiftAffiliateSecretNew : undefined
145+ } )
146+ ) . to . throw ( 'Sideshift config error' )
147+ expect ( ( ) =>
148+ getSideshiftAccounts ( {
149+ sideshiftAffiliateId : 'PRIMARY' ,
150+ sideshiftAffiliateSecret : 'sprimary' ,
151+ sideshiftAffiliateIdNew : undefined ,
152+ sideshiftAffiliateSecretNew : 'sadded'
153+ } )
154+ ) . to . throw ( 'Sideshift config error' )
155+ } )
132156} )
133157
134158describe ( 'querySideshift dual-account merge' , function ( ) {
@@ -299,4 +323,94 @@ describe('querySideshift dual-account merge', function() {
299323 '2025-06-03T00:00:00.000Z'
300324 )
301325 } )
326+
327+ it ( 'advances past a page of only unprocessable orders instead of spinning forever' , async function ( ) {
328+ // Mirrors the live API re-serving the same window: a page containing ONLY
329+ // unprocessable orders keeps coming back until the cursor advances past
330+ // their timestamps. Without advancing the cursor on skip, the loop spins on
331+ // the same startTime until the engine timeout. makeFakeFetcher cannot model
332+ // this (it serves once then empty), so use a startTime-gated fetcher that
333+ // aborts loudly if the loop fails to terminate.
334+ const badDate = '2025-04-10T00:00:00.000Z'
335+ const badTime = new Date ( badDate ) . getTime ( )
336+ let fetchCount = 0
337+ const fetchOrders = async (
338+ account : FakeAccount ,
339+ startTime : number
340+ ) : Promise < unknown [ ] > => {
341+ fetchCount ++
342+ if ( fetchCount > 50 ) throw new Error ( 'query loop did not terminate' )
343+ if ( startTime <= badTime ) return [ { id : 'bad' , createdAt : badDate } ]
344+ return [ ]
345+ }
346+ const alwaysThrows = async ( ) : Promise < StandardTx > => {
347+ throw new Error ( 'Unknown coin: FOO on network bar' )
348+ }
349+
350+ const result = await querySideshift (
351+ {
352+ apiKeys : {
353+ sideshiftAffiliateId : 'PRIMARY' ,
354+ sideshiftAffiliateSecret : 'sprimary'
355+ } ,
356+ settings : { } ,
357+ log : noopLog
358+ } ,
359+ fetchOrders ,
360+ alwaysThrows
361+ )
362+
363+ // No orders are reportable, but the account still advances its cursor past
364+ // the unprocessable window and the loop terminates.
365+ expect ( result . transactions ) . to . deep . equal ( [ ] )
366+ expect ( new Date ( result . settings . accounts . PRIMARY ) . getTime ( ) ) . to . be . at . least (
367+ badTime
368+ )
369+ } )
370+
371+ it ( 'skips an order whose createdAt is unparseable instead of stalling' , async function ( ) {
372+ // When processOrder throws BECAUSE createdAt is malformed, the skip
373+ // handler re-reads createdAt to advance the cursor. That second read must
374+ // not throw again, or the error escapes the skip logic into the retry
375+ // path and the account stalls on this order forever.
376+ const { fetchOrders } = makeFakeFetcher ( {
377+ PRIMARY : [
378+ { orderId : 'good1' , isoDate : '2025-06-01T00:00:00.000Z' } ,
379+ {
380+ orderId : 'bad-date' ,
381+ isoDate : 'not-a-real-date' ,
382+ createdAt : 'not-a-real-date'
383+ } ,
384+ { orderId : 'good2' , isoDate : '2025-06-03T00:00:00.000Z' }
385+ ]
386+ } )
387+ const dateStrictProcess = async ( rawTx : unknown ) : Promise < StandardTx > => {
388+ const order = rawTx as RawOrder
389+ const time = new Date ( order . isoDate ) . getTime ( )
390+ if ( Number . isNaN ( time ) ) {
391+ throw new Error ( `Invalid createdAt: ${ order . isoDate } ` )
392+ }
393+ return makeTx ( order . orderId , order . isoDate )
394+ }
395+ const result = await querySideshift (
396+ {
397+ apiKeys : {
398+ sideshiftAffiliateId : 'PRIMARY' ,
399+ sideshiftAffiliateSecret : 'sprimary'
400+ } ,
401+ settings : { } ,
402+ log : noopLog
403+ } ,
404+ fetchOrders ,
405+ dateStrictProcess
406+ )
407+
408+ expect ( result . transactions . map ( tx => tx . orderId ) ) . to . deep . equal ( [
409+ 'good1' ,
410+ 'good2'
411+ ] )
412+ expect ( result . settings . accounts . PRIMARY ) . to . equal (
413+ '2025-06-03T00:00:00.000Z'
414+ )
415+ } )
302416} )
0 commit comments