Skip to content

Commit 54ee154

Browse files
Adds unit tests for transport nav and improvements
1 parent 11aeead commit 54ee154

File tree

7 files changed

+253
-48
lines changed

7 files changed

+253
-48
lines changed

packages/sources/nav-libre/src/transport/date-utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ export function parseDateString(dateStr: string): Date {
1616
return new Date(Date.UTC(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()))
1717
}
1818

19-
/** Returns a start date that is at most `maxBusinessDays` before `endDate`. */
19+
/**
20+
* Guarantee the (from -> to) span is <= `maxBusinessDays`.
21+
*
22+
* If the gap is larger, shift `from` forward so it sits exactly
23+
* `maxBusinessDays` business days before `to` and returns the new `from`.
24+
*
25+
* Returns the original `from` if the gap is smaller or equal.
26+
*
27+
* Example: 7-day limit
28+
* from = 2025-06-25 (Wed)
29+
* to = 2025-07-10 (Thu)
30+
* span = 11 business days -> newFrom = 2025-07-01
31+
*/
2032
export function clampStartByBusinessDays(
2133
from: Date,
2234
to: Date,

packages/sources/nav-libre/src/transport/fund-dates.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ export interface FundDatesResponse {
88
ToDate: string
99
}
1010

11-
export const getFundDates = async (
12-
globalFundID: number,
13-
baseUrl: string,
14-
apiKey: string,
15-
secret: string,
16-
requester: Requester,
17-
): Promise<FundDatesResponse> => {
11+
export const getFundDates = async ({
12+
globalFundID,
13+
baseURL,
14+
apiKey,
15+
secret,
16+
requester,
17+
}: {
18+
globalFundID: number
19+
baseURL: string
20+
apiKey: string
21+
secret: string
22+
requester: Requester
23+
}): Promise<FundDatesResponse> => {
1824
const method = 'GET'
1925
const url = `/navapigateway/api/v1/ClientMasterData/GetAccountingDataDates?globalFundID=${globalFundID}`
2026
const requestConfig = {
21-
baseURL: baseUrl,
27+
baseURL: baseURL,
2228
url: url,
2329
method: method,
2430
headers: getRequestHeaders(method, url, '', apiKey, secret),

packages/sources/nav-libre/src/transport/fund.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,23 @@ interface FundResponse {
2525
}[]
2626
}
2727

28-
export const getFund = async (
29-
globalFundID: number,
30-
fromDate: string,
31-
toDate: string,
32-
baseURL: string,
33-
apiKey: string,
34-
secret: string,
35-
requester: Requester,
36-
): Promise<FundResponse['Data']> => {
28+
export const getFund = async ({
29+
globalFundID,
30+
fromDate,
31+
toDate,
32+
baseURL,
33+
apiKey,
34+
secret,
35+
requester,
36+
}: {
37+
globalFundID: number
38+
fromDate: string
39+
toDate: string
40+
baseURL: string
41+
apiKey: string
42+
secret: string
43+
requester: Requester
44+
}): Promise<FundResponse['Data']> => {
3745
const method = 'GET'
3846
const url = `/navapigateway/api/v1/FundAccountingData/GetOfficialNAVAndPerformanceReturnsForFund?globalFundID=${globalFundID}&fromDate=${fromDate}&toDate=${toDate}`
3947
// Body is empy for GET

packages/sources/nav-libre/src/transport/nav.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,36 @@ export class NavLibreTransport extends SubscriptionTransport<BaseEndpointTypes>
6060
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
6161
const providerDataRequestedUnixMs = Date.now()
6262
logger.debug(`Handling request for globalFundID: ${param.globalFundID}`)
63-
const { FromDate, ToDate } = await getFundDates(
64-
param.globalFundID,
65-
this.config.API_ENDPOINT,
66-
this.config.API_KEY,
67-
this.config.SECRET_KEY,
68-
this.requester,
63+
const { FromDate: earliestPossibleFromStr, ToDate: latestPossibleToStr } = await getFundDates({
64+
globalFundID: param.globalFundID,
65+
baseURL: this.config.API_ENDPOINT,
66+
apiKey: this.config.API_KEY,
67+
secret: this.config.SECRET_KEY,
68+
requester: this.requester,
69+
})
70+
71+
const earliestPossibleFrom = parseDateString(earliestPossibleFromStr)
72+
const latestPossibleTo = parseDateString(latestPossibleToStr)
73+
74+
// Clamp to trailing-7-business-days window
75+
const preferredFrom = clampStartByBusinessDays(
76+
earliestPossibleFrom,
77+
latestPossibleTo,
78+
7, // 7 business days
6979
)
70-
let from = parseDateString(FromDate)
71-
const to = parseDateString(ToDate)
72-
from = clampStartByBusinessDays(from, to)
7380

74-
logger.debug(`Fetching NAV for globalFundID: ${param.globalFundID} from ${from} to ${to}`)
75-
const fund = await getFund(
76-
param.globalFundID,
77-
toDateString(from),
78-
toDateString(to),
79-
this.config.API_ENDPOINT,
80-
this.config.API_KEY,
81-
this.config.SECRET_KEY,
82-
this.requester,
81+
logger.debug(
82+
`Fetching NAV for globalFundID: ${param.globalFundID} from ${preferredFrom} to ${latestPossibleTo}`,
8383
)
84+
const fund = await getFund({
85+
globalFundID: param.globalFundID,
86+
fromDate: toDateString(preferredFrom),
87+
toDate: toDateString(latestPossibleTo),
88+
baseURL: this.config.API_ENDPOINT,
89+
apiKey: this.config.API_KEY,
90+
secret: this.config.SECRET_KEY,
91+
requester: this.requester,
92+
})
8493

8594
const ACCOUNTING_DATE_KEY = 'Accounting Date'
8695
const NAV_PER_SHARE_KEY = 'NAV Per Share'

packages/sources/nav-libre/test/unit/fund-dates.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ describe('getFundDates', () => {
2020
mockRequester.request = jest.fn().mockResolvedValue({
2121
response: { data: mockResponse },
2222
})
23-
const result = await getFundDates(123, 'http://base', 'apiKey', 'secret', mockRequester)
23+
const result = await getFundDates({
24+
globalFundID: 123,
25+
baseURL: 'http://base',
26+
apiKey: 'apiKey',
27+
secret: 'secret',
28+
requester: mockRequester,
29+
})
30+
2431
expect(result).toEqual(mockResponse)
2532
})
2633

@@ -29,7 +36,13 @@ describe('getFundDates', () => {
2936
response: { data: undefined },
3037
})
3138
await expect(
32-
getFundDates(123, 'http://base', 'apiKey', 'secret', mockRequester),
39+
getFundDates({
40+
globalFundID: 123,
41+
baseURL: 'http://base',
42+
apiKey: 'apiKey',
43+
secret: 'secret',
44+
requester: mockRequester,
45+
}),
3346
).rejects.toThrow()
3447
})
3548
})

packages/sources/nav-libre/test/unit/fund.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ describe('getFund', () => {
3333
data: { Data: mockResponse },
3434
},
3535
})
36-
const result = await getFund(
37-
123,
38-
'01-01-2000',
39-
'01-05-2000',
40-
'http://base',
41-
'apiKey',
42-
'secret',
43-
mockRequester,
44-
)
36+
const result = await getFund({
37+
globalFundID: 123,
38+
fromDate: '01-01-2000',
39+
toDate: '01-05-2000',
40+
baseURL: 'http://base',
41+
apiKey: 'apiKey',
42+
secret: 'secret',
43+
requester: mockRequester,
44+
})
4545
expect(result).toEqual(mockResponse)
4646
})
4747

@@ -50,7 +50,15 @@ describe('getFund', () => {
5050
response: { data: undefined },
5151
})
5252
await expect(
53-
getFund(123, '01-01-2000', '01-05-2000', 'http://base', 'apiKey', 'secret', mockRequester),
53+
getFund({
54+
globalFundID: 123,
55+
fromDate: '01-01-2000',
56+
toDate: '01-05-2000',
57+
baseURL: 'http://base',
58+
apiKey: 'apiKey',
59+
secret: 'secret',
60+
requester: mockRequester,
61+
}),
5462
).rejects.toThrow()
5563
})
5664
})
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
2+
import { LoggerFactoryProvider } from '@chainlink/external-adapter-framework/util'
3+
import { makeStub } from '@chainlink/external-adapter-framework/util/testing-utils'
4+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
5+
6+
import { BaseEndpointTypes, inputParameters as navInputParams } from '../../src/endpoint/nav'
7+
import { NavLibreTransport } from '../../src/transport/nav'
8+
9+
LoggerFactoryProvider.set()
10+
11+
const FUND_ID = 123
12+
const transportName = 'nav_transport'
13+
const endpointName = 'nav'
14+
15+
let transport: NavLibreTransport
16+
17+
// adapter settings stub
18+
const adapterSettings = makeStub('adapterSettings', {
19+
API_ENDPOINT: 'https://api.navfund.com',
20+
API_KEY: 'apiKey',
21+
SECRET_KEY: 'secret',
22+
BACKGROUND_EXECUTE_MS: 0,
23+
MAX_RETRIES: 3,
24+
WARMUP_SUBSCRIPTION_TTL: 10_000,
25+
} as unknown as BaseEndpointTypes['Settings'])
26+
27+
// requester stub that we'll control per‑test
28+
const requester = makeStub('requester', { request: jest.fn() })
29+
const responseCache = makeStub('responseCache', { write: jest.fn() })
30+
const dependencies = makeStub('dependencies', {
31+
requester,
32+
responseCache,
33+
subscriptionSetFactory: { buildSet: jest.fn() },
34+
} as unknown as TransportDependencies<any>)
35+
36+
beforeEach(async () => {
37+
transport = new NavLibreTransport() as unknown as InstanceType<typeof NavLibreTransport>
38+
await transport.initialize(dependencies, adapterSettings, endpointName, transportName)
39+
jest.resetAllMocks()
40+
})
41+
42+
// helper to pull the cached response written in handleRequest
43+
const getCachedResponse = () => (responseCache.write.mock.calls[0][1] as any)[0].response
44+
45+
const FUND_DATES_RES = makeStub('fundDatesRes', {
46+
response: {
47+
data: { LogID: 1, FromDate: '06-01-2025', ToDate: '07-01-2025' },
48+
},
49+
})
50+
51+
const FUND_ROWS = [
52+
{ 'NAV Per Share': 50, 'Accounting Date': '06-10-2025' },
53+
{ 'NAV Per Share': 150, 'Accounting Date': '06-25-2025' },
54+
]
55+
56+
const FUND_RES = makeStub('fundRes', {
57+
response: {
58+
data: { Data: FUND_ROWS },
59+
},
60+
})
61+
62+
describe('NavLibreTransport – handleRequest', () => {
63+
it('returns latest NAV and writes to cache', async () => {
64+
requester.request.mockResolvedValueOnce(FUND_DATES_RES)
65+
requester.request.mockResolvedValueOnce(FUND_RES)
66+
67+
const param = makeStub('param', { globalFundID: FUND_ID } as typeof navInputParams.validated)
68+
69+
await transport.handleRequest({ adapterSettings } as any, param)
70+
71+
expect(responseCache.write).toHaveBeenCalledTimes(1)
72+
73+
const cached = getCachedResponse()
74+
expect(cached).toEqual({
75+
statusCode: 200,
76+
result: 150,
77+
data: {
78+
globalFundID: FUND_ID,
79+
navPerShare: 150,
80+
navDate: '06-25-2025',
81+
},
82+
timestamps: expect.objectContaining({
83+
providerDataRequestedUnixMs: expect.any(Number),
84+
providerDataReceivedUnixMs: expect.any(Number),
85+
providerIndicatedTimeUnixMs: expect.any(Number),
86+
}),
87+
})
88+
89+
expect(requester.request).toHaveBeenCalledTimes(2)
90+
const fundDatesCall = requester.request.mock.calls[0]
91+
expect(fundDatesCall[1]).toMatchObject({
92+
url: expect.stringContaining('/GetAccountingDataDates'),
93+
})
94+
95+
const fundCall = requester.request.mock.calls[1]
96+
expect(fundCall[1]).toMatchObject({
97+
url: expect.stringContaining('/GetOfficialNAVAndPerformanceReturnsForFund'),
98+
})
99+
})
100+
101+
it('maps downstream AdapterError to 502 response', async () => {
102+
requester.request.mockResolvedValueOnce(FUND_DATES_RES) // first OK
103+
requester.request.mockRejectedValueOnce(new AdapterError({ message: 'boom' }))
104+
105+
const param = makeStub('param', { globalFundID: FUND_ID } as typeof navInputParams.validated)
106+
107+
await transport.handleRequest({ adapterSettings } as any, param)
108+
109+
expect(responseCache.write).toHaveBeenCalledTimes(1)
110+
const cached = getCachedResponse()
111+
expect(cached.statusCode).toBe(500)
112+
expect(cached.errorMessage).toContain('boom')
113+
})
114+
115+
it('uses provider earliestFrom when span <= 7 business days', async () => {
116+
const shortSpanDates = makeStub('dates', {
117+
response: { data: { LogID: 1, FromDate: '06-28-2025', ToDate: '07-01-2025' } },
118+
})
119+
requester.request.mockResolvedValueOnce(shortSpanDates)
120+
121+
const fundRows = [
122+
{ 'NAV Per Share': 42, 'Accounting Date': '06-30-2025' },
123+
{ 'NAV Per Share': 43, 'Accounting Date': '07-01-2025' },
124+
]
125+
const fundRes = makeStub('fundRes', { response: { data: { Data: fundRows } } })
126+
requester.request.mockResolvedValueOnce(fundRes)
127+
128+
const param = makeStub('param', { globalFundID: FUND_ID } as typeof navInputParams.validated)
129+
130+
await transport.handleRequest({ adapterSettings } as any, param)
131+
132+
const fundCallCfg = requester.request.mock.calls[1][1]
133+
expect(fundCallCfg.url).toContain('fromDate=06-28-2025')
134+
})
135+
136+
it('caches 400 when Fund rows are empty', async () => {
137+
requester.request.mockResolvedValueOnce(FUND_DATES_RES)
138+
requester.request.mockResolvedValueOnce(
139+
makeStub('emptyFund', { response: { data: { Data: [] } } }),
140+
)
141+
const param = makeStub('param', { globalFundID: FUND_ID } as typeof navInputParams.validated)
142+
143+
await transport.handleRequest({ adapterSettings } as any, param)
144+
145+
const cached = getCachedResponse()
146+
expect(cached.statusCode).toBe(400)
147+
expect(cached.errorMessage).toMatch(/No fund found/i)
148+
})
149+
})

0 commit comments

Comments
 (0)