mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-15 12:50:48 +02:00
* 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>
171 lines
5.8 KiB
TypeScript
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')
|
|
})
|
|
})
|