Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/baseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,17 @@ export class BaseApi {
return ''
}

// Values must be URL-encoded: cursors are base64 and carry '+', '/' and '=', and an unencoded
// '+' reaches the server as a space. Array items are encoded one by one and joined with a
// literal comma, which is how the API expects list params such as `ids`.
const encode = (value: number | string | boolean): string => encodeURIComponent(String(value))
const query = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => key + '=' + params[key])
.map(key => {
const value = params[key]
const encoded = Array.isArray(value) ? value.map(encode).join(',') : encode(value)
return `${encodeURIComponent(key)}=${encoded}`
})
.join('&')
return `?${query}`
}
Expand Down
32 changes: 32 additions & 0 deletions tests/transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,35 @@ describe('creditCardMetadata deserialization', () => {
expect(second.billPostDate).toBeNull()
})
})

describe('query string encoding', () => {
beforeEach(() => {
nock.cleanAll()
setupAuth()
})

it('URL-encodes a base64 cursor so "+", "/" and "=" reach the API intact', async () => {
const cursor = 'MjAyNi0xMC0wNFQxMjowMDowMC4wMDBafGE+Yi9j=='
const mock = nock(API_URL)
.get('/v2/transactions')
.query(actual => actual.after === cursor && actual.accountId === ACCOUNT_ID)
.reply(200, mockCursorPage(['tx-1'], null))

const client = new PluggyClient({ clientId: '123', clientSecret: '456' })
await client.fetchTransactionsCursor(ACCOUNT_ID, { after: cursor })

expect(mock.isDone()).toBeTruthy()
})

it('keeps list params comma-separated', async () => {
const mock = nock(API_URL)
.get('/v2/transactions')
.query(actual => actual.ids === 'a,b' && actual.accountId === ACCOUNT_ID)
.reply(200, mockCursorPage([], null))

const client = new PluggyClient({ clientId: '123', clientSecret: '456' })
await client.fetchTransactionsCursor(ACCOUNT_ID, { ids: ['a', 'b'] })

expect(mock.isDone()).toBeTruthy()
})
})
Loading