mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-21 18:07:46 +02:00
fix(liveuamap): make enrichment resilient and non-blocking
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useLiveUamapScraperOptIn } from '@/hooks/useLiveUamapScraperOptIn';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('useLiveUamapScraperOptIn', () => {
|
||||
it('never blocks Global Incidents when the operator declines LiveUAMap', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
platform_requires_opt_in: true,
|
||||
ui_opted_in: false,
|
||||
ui_choice_recorded: false,
|
||||
scraper_enabled: false,
|
||||
env_override: null,
|
||||
api_configured: false,
|
||||
enrichment_enabled: false,
|
||||
provider_mode: 'gdelt-only',
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
platform_requires_opt_in: true,
|
||||
ui_opted_in: false,
|
||||
ui_choice_recorded: true,
|
||||
scraper_enabled: false,
|
||||
env_override: null,
|
||||
api_configured: false,
|
||||
enrichment_enabled: false,
|
||||
provider_mode: 'gdelt-only',
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
const confirmMock = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useLiveUamapScraperOptIn());
|
||||
await waitFor(() => expect(result.current.status).not.toBeNull());
|
||||
|
||||
let blocked = true;
|
||||
act(() => {
|
||||
blocked = result.current.needsConsentBeforeEnable('global_incidents', true);
|
||||
});
|
||||
|
||||
expect(blocked).toBe(false);
|
||||
expect(confirmMock).toHaveBeenCalledOnce();
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
const [, options] = fetchMock.mock.calls[1];
|
||||
expect(options?.method).toBe('POST');
|
||||
expect(options?.body).toBe(JSON.stringify({ opted_in: false }));
|
||||
});
|
||||
|
||||
it('does not prompt when a supported API provider is configured', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
platform_requires_opt_in: true,
|
||||
ui_opted_in: false,
|
||||
ui_choice_recorded: false,
|
||||
scraper_enabled: false,
|
||||
env_override: null,
|
||||
api_configured: true,
|
||||
enrichment_enabled: true,
|
||||
provider_mode: 'api',
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
const confirmMock = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
const { result } = renderHook(() => useLiveUamapScraperOptIn());
|
||||
await waitFor(() => expect(result.current.status?.api_configured).toBe(true));
|
||||
|
||||
expect(result.current.needsConsentBeforeEnable('global_incidents', true)).toBe(false);
|
||||
expect(confirmMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { API_BASE } from '@/lib/api';
|
||||
|
||||
export type LiveUamapScraperStatus = {
|
||||
@@ -8,10 +8,15 @@ export type LiveUamapScraperStatus = {
|
||||
ui_opted_in: boolean;
|
||||
scraper_enabled: boolean;
|
||||
env_override: 'on' | 'off' | null;
|
||||
ui_choice_recorded?: boolean;
|
||||
api_configured?: boolean;
|
||||
enrichment_enabled?: boolean;
|
||||
provider_mode?: 'api' | 'scraper' | 'gdelt-only';
|
||||
};
|
||||
|
||||
export function useLiveUamapScraperOptIn(enabled = true) {
|
||||
const [status, setStatus] = useState<LiveUamapScraperStatus | null>(null);
|
||||
const choicePromptedRef = useRef(false);
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -29,20 +34,11 @@ export function useLiveUamapScraperOptIn(enabled = true) {
|
||||
void refreshStatus();
|
||||
}, [enabled, refreshStatus]);
|
||||
|
||||
const needsConsentBeforeEnable = useCallback(
|
||||
(layerId: string, turningOn: boolean) =>
|
||||
layerId === 'global_incidents' &&
|
||||
turningOn &&
|
||||
Boolean(status?.platform_requires_opt_in) &&
|
||||
!status?.ui_opted_in,
|
||||
[status],
|
||||
);
|
||||
|
||||
const confirmOptIn = useCallback(async () => {
|
||||
const setOptIn = useCallback(async (optedIn: boolean) => {
|
||||
const res = await fetch(`${API_BASE}/api/liveuamap/scraper-opt-in`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ opted_in: true }),
|
||||
body: JSON.stringify({ opted_in: optedIn }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`LiveUAMap opt-in failed (${res.status})`);
|
||||
@@ -52,6 +48,45 @@ export function useLiveUamapScraperOptIn(enabled = true) {
|
||||
return body;
|
||||
}, []);
|
||||
|
||||
const needsConsentBeforeEnable = useCallback(
|
||||
(layerId: string, turningOn: boolean) => {
|
||||
if (layerId !== 'global_incidents' || !turningOn) return false;
|
||||
|
||||
const choiceRecorded = status?.ui_choice_recorded ?? status?.ui_opted_in ?? false;
|
||||
const shouldOfferBrowserEnrichment =
|
||||
Boolean(status?.platform_requires_opt_in) &&
|
||||
!choiceRecorded &&
|
||||
!status?.api_configured &&
|
||||
status?.env_override === null;
|
||||
|
||||
if (
|
||||
shouldOfferBrowserEnrichment &&
|
||||
!choicePromptedRef.current &&
|
||||
typeof window !== 'undefined'
|
||||
) {
|
||||
choicePromptedRef.current = true;
|
||||
const optedIn = window.confirm(
|
||||
"Global Incidents will turn on with GDELT either way. Add optional LiveUAMap pins too? LiveUAMap will see this server's IP. OK enables LiveUAMap; Cancel keeps GDELT-only incidents.",
|
||||
);
|
||||
|
||||
// Do not make the Global Incidents toggle wait on an optional provider.
|
||||
// Give the layer-state update a moment to reach the backend before the
|
||||
// opt-in endpoint opportunistically starts an immediate refresh.
|
||||
window.setTimeout(() => {
|
||||
void setOptIn(optedIn).catch((error) => {
|
||||
console.warn('LiveUAMap preference update failed:', error);
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// LiveUAMap is enrichment, never a prerequisite for Global Incidents.
|
||||
return false;
|
||||
},
|
||||
[setOptIn, status],
|
||||
);
|
||||
|
||||
const confirmOptIn = useCallback(() => setOptIn(true), [setOptIn]);
|
||||
|
||||
return {
|
||||
status,
|
||||
refreshStatus,
|
||||
|
||||
Reference in New Issue
Block a user