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
22 changes: 19 additions & 3 deletions src/__tests__/usePolicies.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ describe('usePolicies', () => {
expect(hook.current.error).toBeNull();
});

// #525: the wallet must not leak into the /policies query string -- the
// backend resolves it from the JWT, so the API call takes no wallet arg.
it('does not pass the wallet address to fetchUserPolicies', async () => {
fetchUserPolicies.mockResolvedValue([]);

renderHook(() => usePolicies('GWALLET'));
await flushMicrotasks();

expect(fetchUserPolicies).toHaveBeenCalledTimes(1);
expect(fetchUserPolicies).toHaveBeenCalledWith();
});

it('surfaces an error message when the fetch fails', async () => {
fetchUserPolicies.mockRejectedValue(new Error('failed to reach api'));

Expand Down Expand Up @@ -88,13 +100,17 @@ describe('usePolicies', () => {
});

it('discards a stale response from a previous wallet after the wallet address changes', async () => {
// fetchUserPolicies takes no wallet arg (#525 -- the backend reads it from
// the JWT), so key pending requests by the wallet active at call time.
const pending: Record<string, (policies: Policy[]) => void> = {};
let wallet = 'GWALLET_A';
fetchUserPolicies.mockImplementation(
(wallet: string) =>
new Promise<Policy[]>((resolve) => { pending[wallet] = resolve; }),
() => {
const forWallet = wallet;
return new Promise<Policy[]>((resolve) => { pending[forWallet] = resolve; });
},
);

let wallet = 'GWALLET_A';
const hook = renderHook(() => usePolicies(wallet));
await flushMicrotasks();
expect(pending['GWALLET_A']).toBeDefined();
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/usePolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function usePolicies(walletAddress: string | null) {
}
setError(null);
try {
const data = await fetchUserPolicies(walletAddress);
const data = await fetchUserPolicies();
if (signal.aborted) return;
setPolicies(data);
} catch (err) {
Expand Down
10 changes: 8 additions & 2 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,14 @@ export function fetchProduct(id: string): Promise<Product> {

// ── Policies ──────────────────────────────────────────────────────────────────

export function fetchUserPolicies(wallet: string): Promise<Policy[]> {
return get<Policy[]>('/policies', { params: { wallet } });
/**
* Policies for the authenticated wallet. The backend resolves the wallet from
* the JWT (backend #345), so it is deliberately NOT sent as a `?wallet=` query
* param -- query strings end up in server/proxy access logs and browser
* history, leaking the user's address in plaintext (#525).
*/
export function fetchUserPolicies(): Promise<Policy[]> {
return get<Policy[]>('/policies');
}

export function fetchPolicy(id: string): Promise<Policy> {
Expand Down
Loading