Skip to content

Commit a7284d8

Browse files
Adds unit tests, addreses feedback
1 parent 721370c commit a7284d8

File tree

7 files changed

+165
-16
lines changed

7 files changed

+165
-16
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export function parseDateString(dateStr: string): Date {
1313
if (!isValid(parsed)) {
1414
throw new Error(`date must be in ${DATE_FORMAT} format: got "${dateStr}"`)
1515
}
16-
return parsed
16+
return new Date(Date.UTC(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()))
1717
}
1818

19-
/** Ensure the window is <= N business days. Returns the (possibly adjusted) from-date. */
20-
export function clampToBusinessWindow(
19+
/** Returns a start date that is at most `maxBusinessDays` before `endDate`. */
20+
export function clampStartByBusinessDays(
2121
from: Date,
2222
to: Date,
2323
maxBusinessDays = MAX_BUSINESS_DAYS,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Requester } from '@chainlink/external-adapter-framework/util/requester'
22
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
33
import { getRequestHeaders } from './authentication'
44

5-
interface FundDatesResponse {
5+
export interface FundDatesResponse {
66
LogID: number
77
FromDate: string
88
ToDate: string
@@ -14,7 +14,7 @@ export const getFundDates = async (
1414
apiKey: string,
1515
secret: string,
1616
requester: Requester,
17-
) => {
17+
): Promise<FundDatesResponse> => {
1818
const method = 'GET'
1919
const url = `/navapigateway/api/v1/ClientMasterData/GetAccountingDataDates?globalFundID=${globalFundID}`
2020
const requestConfig = {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const getFund = async (
3333
apiKey: string,
3434
secret: string,
3535
requester: Requester,
36-
) => {
36+
): Promise<FundResponse['Data']> => {
3737
const method = 'GET'
3838
const url = `/navapigateway/api/v1/FundAccountingData/GetOfficialNAVAndPerformanceReturnsForFund?globalFundID=${globalFundID}&fromDate=${fromDate}&toDate=${toDate}`
3939
// Body is empy for GET

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { SubscriptionTransport } from '@chainlink/external-adapter-framework/tra
88
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
99
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
1010
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
11-
import { clampToBusinessWindow, parseDateString, toDateString } from './date-utils'
11+
import { clampStartByBusinessDays, parseDateString, toDateString } from './date-utils'
1212
const logger = makeLogger('NavLibreTransport')
1313

1414
type RequestParams = typeof inputParameters.validated
@@ -69,7 +69,7 @@ export class NavLibreTransport extends SubscriptionTransport<BaseEndpointTypes>
6969
)
7070
let from = parseDateString(FromDate)
7171
const to = parseDateString(ToDate)
72-
from = clampToBusinessWindow(from, to)
72+
from = clampStartByBusinessDays(from, to)
7373

7474
logger.debug(`Fetching NAV for globalFundID: ${param.globalFundID} from ${from} to ${to}`)
7575
const fund = await getFund(
@@ -82,20 +82,23 @@ export class NavLibreTransport extends SubscriptionTransport<BaseEndpointTypes>
8282
this.requester,
8383
)
8484

85+
const ACCOUNTING_DATE_KEY = 'Accounting Date'
86+
const NAV_PER_SHARE_KEY = 'NAV Per Share'
8587
// Find the latest NAV entry by Accounting Date
86-
const latest = fund.reduce((a, b) => {
87-
return new Date(a['Accounting Date']) > new Date(b['Accounting Date']) ? a : b
88-
})
89-
const [month, day, year] = latest['Accounting Date'].split('-').map(Number)
88+
const latest = fund.reduce((latestRow, row) =>
89+
parseDateString(row[ACCOUNTING_DATE_KEY]) > parseDateString(latestRow[ACCOUNTING_DATE_KEY])
90+
? row
91+
: latestRow,
92+
)
9093
// Assumes UTC
91-
const providerIndicatedTimeUnixMs = Date.UTC(year, month - 1, day) // month is 0-based
94+
const providerIndicatedTimeUnixMs = parseDateString(latest[ACCOUNTING_DATE_KEY]).getTime()
9295
return {
9396
statusCode: 200,
94-
result: latest['NAV Per Share'],
97+
result: latest[NAV_PER_SHARE_KEY],
9598
data: {
9699
globalFundID: param.globalFundID,
97-
navPerShare: latest['NAV Per Share'],
98-
navDate: latest['Accounting Date'],
100+
navPerShare: latest[NAV_PER_SHARE_KEY],
101+
navDate: latest[ACCOUNTING_DATE_KEY],
99102
},
100103
timestamps: {
101104
providerDataRequestedUnixMs,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { differenceInBusinessDays, parse } from 'date-fns'
2+
import {
3+
clampStartByBusinessDays,
4+
DATE_FORMAT,
5+
MAX_BUSINESS_DAYS,
6+
parseDateString,
7+
toDateString,
8+
} from '../../src/transport/date-utils'
9+
10+
describe('date-utils', () => {
11+
// Force UTC so tests behave the same everywhere
12+
beforeAll(() => {
13+
process.env.TZ = 'UTC'
14+
})
15+
16+
describe('parseDateString', () => {
17+
it('parses a valid MM-dd-yyyy string', () => {
18+
const input = '07-11-2025'
19+
const parsed = parseDateString(input)
20+
expect(parsed).toBeInstanceOf(Date)
21+
expect(parsed.getUTCFullYear()).toBe(2025)
22+
expect(parsed.getUTCMonth()).toBe(6) // July is 6 (zero‑based)
23+
expect(parsed.getUTCDate()).toBe(11)
24+
})
25+
26+
it('throws for malformed input', () => {
27+
const badInput = '2025-07-11'
28+
expect(() => parseDateString(badInput)).toThrow(
29+
`date must be in ${DATE_FORMAT} format: got "${badInput}"`,
30+
)
31+
})
32+
})
33+
34+
describe('clampToBusinessWindow', () => {
35+
const to = parse('07-15-2025', DATE_FORMAT, new Date())
36+
37+
it('returns the original from date when within the limit', () => {
38+
const from = parse('07-07-2025', DATE_FORMAT, new Date()) // 6 business days before
39+
const result = clampStartByBusinessDays(from, to, MAX_BUSINESS_DAYS)
40+
expect(result).toEqual(from)
41+
})
42+
43+
it('clamps when span exceeds the limit', () => {
44+
const from = parse('07-01-2025', DATE_FORMAT, new Date()) // >7 business days
45+
const result = clampStartByBusinessDays(from, to, MAX_BUSINESS_DAYS)
46+
// Should be exactly MAX_BUSINESS_DAYS business days before `to`
47+
const span = differenceInBusinessDays(to, result)
48+
expect(span).toBe(MAX_BUSINESS_DAYS)
49+
})
50+
})
51+
52+
describe('toDateString', () => {
53+
it('formats a Date back to MM-dd-yyyy', () => {
54+
const date = new Date(Date.UTC(2025, 6, 11))
55+
const formatted = toDateString(date)
56+
expect(formatted).toBe('07-11-2025')
57+
})
58+
})
59+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
2+
import { FundDatesResponse, getFundDates } from '../../src/transport/fund-dates'
3+
4+
describe('getFundDates', () => {
5+
const mockRequester = {
6+
request: jest.fn(),
7+
} as unknown as Requester
8+
9+
const mockResponse: FundDatesResponse = {
10+
LogID: 1,
11+
FromDate: '01-01-2023',
12+
ToDate: '01-31-2023',
13+
}
14+
15+
beforeEach(() => {
16+
jest.clearAllMocks()
17+
})
18+
19+
it('returns fund dates on success', async () => {
20+
mockRequester.request = jest.fn().mockResolvedValue({
21+
response: { data: mockResponse },
22+
})
23+
const result = await getFundDates(123, 'http://base', 'apiKey', 'secret', mockRequester)
24+
expect(result).toEqual(mockResponse)
25+
})
26+
27+
it('throws if no data returned', async () => {
28+
mockRequester.request = jest.fn().mockResolvedValue({
29+
response: { data: undefined },
30+
})
31+
await expect(
32+
getFundDates(123, 'http://base', 'apiKey', 'secret', mockRequester),
33+
).rejects.toThrow()
34+
})
35+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
2+
import * as authModule from '../../src/transport/authentication'
3+
import { getFund } from '../../src/transport/fund'
4+
5+
describe('getFund', () => {
6+
const mockRequester = {
7+
request: jest.fn(),
8+
} as unknown as Requester
9+
10+
const mockResponse = [
11+
{
12+
'Accounting Date': '01-01-2023',
13+
'NAV Per Share': 123.45,
14+
},
15+
]
16+
17+
let getRequestHeadersStub: jest.SpyInstance
18+
beforeEach(() => {
19+
jest.clearAllMocks()
20+
getRequestHeadersStub = jest.spyOn(authModule, 'getRequestHeaders').mockReturnValue({})
21+
})
22+
23+
afterEach(() => {
24+
getRequestHeadersStub.mockRestore()
25+
})
26+
it('returns fund data on success', async () => {
27+
mockRequester.request = jest.fn().mockResolvedValue({
28+
response: {
29+
data: { Data: mockResponse },
30+
},
31+
})
32+
const result = await getFund(
33+
123,
34+
'01-01-2000',
35+
'01-05-2000',
36+
'http://base',
37+
'apiKey',
38+
'secret',
39+
mockRequester,
40+
)
41+
expect(result).toEqual(mockResponse)
42+
})
43+
44+
it('throws if no data returned', async () => {
45+
mockRequester.request = jest.fn().mockResolvedValue({
46+
response: { data: undefined },
47+
})
48+
await expect(
49+
getFund(123, '01-01-2000', '01-05-2000', 'http://base', 'apiKey', 'secret', mockRequester),
50+
).rejects.toThrow()
51+
})
52+
})

0 commit comments

Comments
 (0)