Skip to content

Commit b719806

Browse files
authored
fix(welcome): include bearer token in core RPC test-connection probe (tinyhumansai#1301)
1 parent a35ba10 commit b719806

3 files changed

Lines changed: 98 additions & 6 deletions

File tree

app/src/pages/Welcome.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useState } from 'react';
33
import OAuthProviderButton from '../components/oauth/OAuthProviderButton';
44
import { oauthProviderConfigs } from '../components/oauth/providerConfigs';
55
import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas';
6-
import { clearCoreRpcUrlCache } from '../services/coreRpcClient';
6+
import { clearCoreRpcUrlCache, testCoreRpcConnection } from '../services/coreRpcClient';
77
import { useDeepLinkAuthState } from '../store/deepLinkAuthState';
88
import {
99
clearStoredRpcUrl,
@@ -65,11 +65,7 @@ const Welcome = () => {
6565
setRpcUrlError(null);
6666

6767
try {
68-
const response = await fetch(normalized, {
69-
method: 'POST',
70-
headers: { 'Content-Type': 'application/json' },
71-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'openhuman.ping', params: {} }),
72-
});
68+
const response = await testCoreRpcConnection(normalized);
7369

7470
if (response.ok || response.status === 405) {
7571
setSaveSuccess(true);

app/src/services/__tests__/coreRpcClient.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,76 @@ describe('coreRpcClient', () => {
421421
expect(tokenCalls).toBe(1);
422422
expect(fetch).not.toHaveBeenCalled();
423423
});
424+
425+
describe('testCoreRpcConnection', () => {
426+
test('POSTs an openhuman.ping JSON-RPC envelope to the supplied URL', async () => {
427+
vi.resetModules();
428+
vi.mocked(isTauri).mockReturnValue(false);
429+
const { testCoreRpcConnection } = await import('../coreRpcClient');
430+
const fetchMock = vi.mocked(fetch);
431+
fetchMock.mockResolvedValueOnce({ ok: true, status: 200 } as Response);
432+
433+
await testCoreRpcConnection('http://example.test:7788/rpc');
434+
435+
expect(fetchMock).toHaveBeenCalledTimes(1);
436+
const [url, init] = fetchMock.mock.calls[0];
437+
expect(url).toBe('http://example.test:7788/rpc');
438+
const requestInit = init as RequestInit;
439+
expect(requestInit.method).toBe('POST');
440+
expect(JSON.parse(requestInit.body as string)).toMatchObject({
441+
jsonrpc: '2.0',
442+
id: 1,
443+
method: 'openhuman.ping',
444+
params: {},
445+
});
446+
});
447+
448+
test('omits Authorization header when no bearer token is available (non-Tauri)', async () => {
449+
vi.resetModules();
450+
vi.mocked(isTauri).mockReturnValue(false);
451+
const { testCoreRpcConnection } = await import('../coreRpcClient');
452+
const fetchMock = vi.mocked(fetch);
453+
fetchMock.mockResolvedValueOnce({ ok: true, status: 200 } as Response);
454+
455+
await testCoreRpcConnection('http://example.test:7788/rpc');
456+
457+
const requestInit = fetchMock.mock.calls[0][1] as RequestInit;
458+
const headers = requestInit.headers as Record<string, string>;
459+
expect(headers).toMatchObject({ 'Content-Type': 'application/json' });
460+
expect(headers).not.toHaveProperty('Authorization');
461+
});
462+
463+
test('attaches Authorization: Bearer when the Tauri bearer token resolves', async () => {
464+
vi.resetModules();
465+
vi.mocked(isTauri).mockReturnValue(true);
466+
vi.mocked(invoke).mockImplementation(async (cmd: string) => {
467+
if (cmd === 'core_rpc_token') return 'deadbeef';
468+
throw new Error(`unexpected command: ${cmd}`);
469+
});
470+
const { testCoreRpcConnection } = await import('../coreRpcClient');
471+
const fetchMock = vi.mocked(fetch);
472+
fetchMock.mockResolvedValueOnce({ ok: true, status: 200 } as Response);
473+
474+
await testCoreRpcConnection('http://example.test:7788/rpc');
475+
476+
const requestInit = fetchMock.mock.calls[0][1] as RequestInit;
477+
const headers = requestInit.headers as Record<string, string>;
478+
expect(headers.Authorization).toBe('Bearer deadbeef');
479+
expect(headers['Content-Type']).toBe('application/json');
480+
});
481+
482+
test('returns the raw fetch Response so callers can inspect status/ok', async () => {
483+
vi.resetModules();
484+
vi.mocked(isTauri).mockReturnValue(false);
485+
const { testCoreRpcConnection } = await import('../coreRpcClient');
486+
const fetchMock = vi.mocked(fetch);
487+
const probe = { ok: false, status: 405, statusText: 'Method Not Allowed' } as Response;
488+
fetchMock.mockResolvedValueOnce(probe);
489+
490+
const response = await testCoreRpcConnection('http://example.test:7788/rpc');
491+
492+
expect(response).toBe(probe);
493+
expect(response.status).toBe(405);
494+
});
495+
});
424496
});

app/src/services/coreRpcClient.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ async function getCoreRpcToken(): Promise<string | null> {
167167
return resolvingCoreRpcToken;
168168
}
169169

170+
/**
171+
* Probe an arbitrary core RPC URL with `openhuman.ping`. Used by the
172+
* Welcome page's "Test Connection" affordance to validate a user-entered
173+
* RPC URL without going through the cached `getCoreRpcUrl` resolution.
174+
*
175+
* Encapsulates the bearer-token + JSON-RPC envelope assembly that would
176+
* otherwise sit in the calling component, keeping all RPC client behavior
177+
* inside the service per the project guideline ("Keep Tauri IPC and RPC
178+
* client calls localized to services … do not scatter `invoke()` or
179+
* direct RPC calls throughout components").
180+
*/
181+
export async function testCoreRpcConnection(url: string): Promise<Response> {
182+
const token = await getCoreRpcToken();
183+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
184+
if (token) {
185+
headers.Authorization = `Bearer ${token}`;
186+
}
187+
return fetch(url, {
188+
method: 'POST',
189+
headers,
190+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'openhuman.ping', params: {} }),
191+
});
192+
}
193+
170194
export async function getCoreHttpBaseUrl(): Promise<string> {
171195
const rpcUrl = await getCoreRpcUrl();
172196
const url = new URL(rpcUrl);

0 commit comments

Comments
 (0)