jam/src/lib/config.test.ts
Kishore B 7cf31890b2
refactor: migrate API client to joinmarket-ng-api-ts (#1305)
* chore(deps): migrate to joinmarket-ng-api-ts

- Uninstall legacy  (v0.3.0).
- Install new  (v1.0.0).
- Update package.json  for  to apply to the new ng-api package.

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor: update all api client imports to use joinmarket-ng-api-ts

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor(types): replace ErrorMessage with unknown for ng-api compatibility

- Remove ErrorMessage type imports from @joinmarket-webui/joinmarket-ng-api-ts/jm across the codebase.
- Update error types in React Query hooks (useQuery, useMutation) and context states to use unknown instead of ErrorMessage.
- Update error handler callbacks (onError) to accept unknown type, adapting to the error type changes in the generated ng-api client.

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* fix(types): handle nullable fields from ng-api client

- Add nullish/undefined checks for sessionInfo.wallet_name in create, import, and login flows.
- Handle nullable progress field in rescanInfo by checking != null and defaulting to undefined.
- Fallback block_height to undefined if nullish in layout and session contexts.
- Default walletInfo.accounts to an empty array to prevent mapping errors if nullish.

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* fix(types): fix typescript errors from ng-api transition

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* fix(lint): resolve typescript-eslint errors in tests and providers

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* feat: redesign sweep to tumbler api with minimal adaptations

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* pin api-ts deps and validate rescan progress value

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* simplify active session check in import flows and harden report parsing

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor: fastapi error parsing and SweepPage ux

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* fix: set VITE_JM_API_BASE_URL to empty for joinmarket-ng-api-ts
-Endpoints generated by the new client package already include the
'/api/v1' path prefix in their definitions.

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor(ui): use session state as single source of truth for active wallets

- Use  as the sole condition to display the active session warning in wallet create/import flows, rather than relying on the presence of a .
- Update  to dynamically fall back to a localized placeholder ('another wallet') when  is missing.
- Add  fallback key to English translation file.

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* style: format create, import files

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor(earn): support only ng backend format in yieldgen report hook

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor: improve active wallet warning translations

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* fix(sweep): use includes instead of repeated equality checks for lint check

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

* refactor(sweep): align phase completion check strictly with ng backend PhaseStatus

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>

---------

Signed-off-by: kishore08-07 <kishorebsm8@gmail.com>
2026-07-10 20:04:16 +05:30

171 lines
5.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'
import { authStore } from '@/store/authStore'
import { buildAuthHeader, buildAuthHeaderMap, createApiClient, unauthorizedResponseInterceptor } from './config'
import { queryClient } from './queryClient'
const mocks = vi.hoisted(() => ({
createClient: vi.fn(),
requestUse: vi.fn(),
responseUse: vi.fn(),
errorUse: vi.fn(),
isDevMode: vi.fn(),
}))
vi.mock('@joinmarket-webui/joinmarket-ng-api-ts', () => ({
createClient: mocks.createClient,
}))
vi.mock('@/constants/debugFeatures', () => ({
isDevMode: mocks.isDevMode,
}))
const createMockClient = () => ({
interceptors: {
request: { use: mocks.requestUse },
response: { use: mocks.responseUse },
error: { use: mocks.errorUse },
},
})
describe('auth header helpers', () => {
it('should build tuple and map authorization headers', () => {
expect(buildAuthHeader('token-123')).toEqual(['x-jm-authorization', 'Bearer token-123'])
expect(buildAuthHeaderMap('token-123')).toEqual({ 'x-jm-authorization': 'Bearer token-123' })
})
})
describe('createApiClient', () => {
beforeEach(() => {
mocks.createClient.mockReturnValue(createMockClient())
mocks.requestUse.mockReset()
mocks.responseUse.mockReset()
mocks.errorUse.mockReset()
mocks.isDevMode.mockReturnValue(false)
authStore.getState().clear()
})
it('should configure client middleware and interceptors', () => {
const client = createApiClient()
expect(client).toBe(mocks.createClient.mock.results[0].value)
expect(mocks.createClient).toHaveBeenCalledWith({ baseUrl: String(import.meta.env.VITE_JM_API_BASE_URL) })
expect(mocks.requestUse).toHaveBeenCalledTimes(1)
expect(mocks.responseUse).toHaveBeenCalledWith(unauthorizedResponseInterceptor)
expect(mocks.errorUse).toHaveBeenCalledTimes(1)
})
it('should attach wallet auth token to outgoing requests', () => {
createApiClient()
authStore.getState().update({
walletFileName: 'test.jmdat',
auth: { token: 'tok', refresh_token: 'ref' },
})
const request = new Request('https://example.test')
const authMiddleware = mocks.requestUse.mock.calls[0][0] as (request: Request) => Request
expect(authMiddleware(request)).toBe(request)
expect(request.headers.get('x-jm-authorization')).toBe('Bearer tok')
})
it('should leave requests unchanged when no auth token exists', () => {
createApiClient()
const request = new Request('https://example.test')
const authMiddleware = mocks.requestUse.mock.calls[0][0] as (request: Request) => Request
expect(authMiddleware(request)).toBe(request)
expect(request.headers.has('x-jm-authorization')).toBe(false)
})
it('should register logging interceptors in dev mode', () => {
const debug = vi.spyOn(console, 'debug').mockImplementation(() => undefined)
mocks.isDevMode.mockReturnValue(true)
createApiClient()
debug.mockClear()
expect(mocks.requestUse).toHaveBeenCalledTimes(2)
expect(mocks.responseUse).toHaveBeenCalledTimes(2)
const request = new Request('https://example.test')
const response = new Response(null, { status: 204 })
expect((mocks.requestUse.mock.calls[1][0] as (request: Request) => Request)(request)).toBe(request)
expect((mocks.responseUse.mock.calls[1][0] as (response: Response) => Response)(response)).toBe(response)
expect(debug).toHaveBeenCalledTimes(2)
debug.mockRestore()
})
it('should normalize intercepted errors', () => {
createApiClient()
const normalizeError = mocks.errorUse.mock.calls[0][0] as (error: unknown) => { message: string }
expect(normalizeError(new Error('boom')).message).toBe('boom')
})
})
describe('unauthorizedResponseInterceptor', () => {
let queryClientClearSpy: MockInstance<typeof queryClient.clear>
beforeEach(() => {
queryClientClearSpy = vi.spyOn(queryClient, 'clear')
mocks.createClient.mockReturnValue(createMockClient())
mocks.requestUse.mockReset()
mocks.responseUse.mockReset()
mocks.errorUse.mockReset()
mocks.isDevMode.mockReturnValue(false)
authStore.getState().update({
walletFileName: 'test.jmdat',
auth: { token: 'tok', refresh_token: 'ref' },
})
queryClientClearSpy.mockReset()
})
afterEach(() => {
queryClientClearSpy.mockRestore()
})
it('should clear auth on invalid-token 401', () => {
expect(authStore.getState().state?.auth?.token).toBe('tok')
expect(queryClientClearSpy).toBeCalledTimes(0)
unauthorizedResponseInterceptor(
new Response(null, {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer, error="invalid_token", error_description="Invalid token."' },
}),
)
expect(authStore.getState().state).toBeUndefined()
expect(queryClientClearSpy).toBeCalledTimes(1)
})
it('should return the response after clearing', () => {
const response = new Response(null, {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer, error="invalid_token", error_description="Invalid token."' },
})
const result = unauthorizedResponseInterceptor(response)
expect(result).toBe(response)
})
it('should not clear auth on non-auth 401 responses', () => {
unauthorizedResponseInterceptor(new Response(null, { status: 401 }))
unauthorizedResponseInterceptor(
new Response(null, {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer, error="service_state", error_description="Not running."' },
}),
)
expect(authStore.getState().state?.auth?.token).toBe('tok')
})
it('should not clear auth on non-401 responses', () => {
unauthorizedResponseInterceptor(new Response(null, { status: 200 }))
unauthorizedResponseInterceptor(new Response(null, { status: 403 }))
unauthorizedResponseInterceptor(new Response(null, { status: 500 }))
expect(authStore.getState().state?.auth?.token).toBe('tok')
})
})