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
15 changes: 13 additions & 2 deletions __tests__/priceStore.cadence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { SelectServerEnum } from '../app/AppState';
import { getZecPrice } from '../app/walletBackend';
import { mockInfo } from '../__mocks__/dataMocks/mockInfo';
import { INITIAL_MIXNET_VIEW } from '../app/walletBackend/transforms/mixnetView';

const price = getZecPrice as jest.MockedFunction<typeof getZecPrice>;

Expand Down Expand Up @@ -92,11 +93,21 @@ test('turning Nym on mid-session fetches at once', async () => {
price.mockResolvedValue({ price: 42, error: '' });
const setZecPrice = jest.fn();

const view = render(driverUi(makeCtx({ nym: false }), setZecPrice));
const view = render(
driverUi(
makeCtx({ nym: false, mixnetView: INITIAL_MIXNET_VIEW }),
setZecPrice,
),
);
await jest.advanceTimersByTimeAsync(10_000);
expect(price).not.toHaveBeenCalled();

view.rerender(driverUi(makeCtx({ nym: true }), setZecPrice));
view.rerender(
driverUi(
makeCtx({ nym: true, mixnetView: INITIAL_MIXNET_VIEW }),
setZecPrice,
),
);
await jest.advanceTimersByTimeAsync(0);
expect(price).toHaveBeenCalledTimes(1);
});
Expand Down
7 changes: 4 additions & 3 deletions __tests__/priceStore.lifecycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,14 +467,15 @@ test('rapid app hops inside the cooldown do not multiply fetches', async () => {
expect(price).toHaveBeenCalledTimes(1);
});

test('without the Nym selection no price traffic exists', async () => {
test('Nym off holds off clearnet while the route still serves', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: 42, error: '' });
const setZecPrice = jest.fn();
seedDeps(setZecPrice);

// USD may even be the seeded default; the currency authorizes nothing.
render(fetcherUi(makeCtx({ nym: false }), setZecPrice));
render(
fetcherUi(makeCtx({ nym: false, mixnetView: READY_VIEW }), setZecPrice),
);
await jest.advanceTimersByTimeAsync(61_000);
fireAppState('background');
fireAppState('active');
Expand Down
48 changes: 48 additions & 0 deletions __tests__/priceStore.optIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,23 @@ import {
import { CurrencyEnum, SelectServerEnum } from '../app/AppState';
import { getZecPrice } from '../app/walletBackend';
import { mockInfo } from '../__mocks__/dataMocks/mockInfo';
import {
MIXNET_STATUS_KEYS,
MixnetStatusKey,
MixnetView,
} from '../app/walletBackend/transforms/mixnetView';

const price = getZecPrice as jest.MockedFunction<typeof getZecPrice>;

const viewFor = (statusKey: MixnetStatusKey): MixnetView => ({
statusKey,
socks5Addr: null,
narration: null,
sendBlocked: true,
recovery: 'none',
reconnecting: false,
});

type Ctx = typeof defaultAppContextLoaded;
const makeCtx = (over?: Partial<Ctx>): Ctx => ({
...defaultAppContextLoaded,
Expand Down Expand Up @@ -158,6 +172,40 @@ test('a return parked on a flight still arms the hop rate bound', async () => {
expect(price).toHaveBeenCalledTimes(4); // rate-bound, no fifth call
});

const FETCH_EXPECTED: Record<string, boolean> = {
'true|mixnet.status.off': false,
'true|mixnet.status.bootstrapping': true,
'true|mixnet.status.ready': true,
'true|mixnet.status.died': false,
'true|mixnet.status.unknown': true,
'false|mixnet.status.off': true,
'false|mixnet.status.bootstrapping': false,
'false|mixnet.status.ready': false,
'false|mixnet.status.died': false,
'false|mixnet.status.unknown': true,
};

test('the opt-in resolves a fetch for every mixnet status', async () => {
for (const nym of [true, false]) {
for (const statusKey of MIXNET_STATUS_KEYS) {
jest.useFakeTimers();
price.mockReset();
price.mockResolvedValue({ price: 42, error: '' });
priceFetcherStore.resetForTests();
const setZecPrice = jest.fn();

const view = render(
surfaceUi(makeCtx({ nym, mixnetView: viewFor(statusKey) }), setZecPrice),
);
await jest.advanceTimersByTimeAsync(0);

expect(price.mock.calls.length > 0).toBe(FETCH_EXPECTED[`${nym}|${statusKey}`]);
view.unmount();
jest.useRealTimers();
}
}
});

test('a re-render behind the closed gate emits no traffic', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: 42, error: '' });
Expand Down
48 changes: 47 additions & 1 deletion __tests__/priceStore.trafficGuards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ const DIED_VIEW: MixnetView = {
reconnecting: false,
};

const UNKNOWN_VIEW: MixnetView = {
statusKey: 'mixnet.status.unknown',
socks5Addr: null,
narration: null,
sendBlocked: true,
recovery: 'reenable',
reconnecting: false,
};

const price = getZecPrice as jest.MockedFunction<typeof getZecPrice>;

type Ctx = typeof defaultAppContextLoaded;
Expand Down Expand Up @@ -144,7 +153,12 @@ test('a withdrawn opt-in stops the mid-flight retry', async () => {
const view = render(surfaceUi(makeCtx(), setZecPrice));
await waitFor(() => expect(price).toHaveBeenCalledTimes(1));

view.rerender(surfaceUi(makeCtx({ nym: false }), setZecPrice)); // Nym off
view.rerender(
surfaceUi(
makeCtx({ nym: false, mixnetView: INITIAL_MIXNET_VIEW }),
setZecPrice,
),
);
land({ price: -1, error: 'refused' });
await flush();

Expand Down Expand Up @@ -204,6 +218,38 @@ test('a switch-off fetches the price over clearnet', async () => {
);
});

test('a failed disable still fetches the price over clearnet', async () => {
price.mockResolvedValue({ price: 42, error: '' });
const setZecPrice = jest.fn();

render(
surfaceUi(
makeCtx({ nym: false, mixnetView: UNKNOWN_VIEW }),
setZecPrice,
),
);
await waitFor(() =>
expect(setZecPrice).toHaveBeenCalledWith(42, expect.any(Number)),
);
});

test('a disabled route that still refuses keeps retrying, not dying', async () => {
jest.useFakeTimers();
price.mockResolvedValue({ price: -1, error: 'refused' });
const setZecPrice = jest.fn();

render(
surfaceUi(makeCtx({ nym: false, mixnetView: UNKNOWN_VIEW }), setZecPrice),
);
await jest.advanceTimersByTimeAsync(0);
const entryCalls = price.mock.calls.length;
expect(entryCalls).toBeGreaterThan(0);

await jest.advanceTimersByTimeAsync(21 * 60_000);
expect(price.mock.calls.length).toBeGreaterThan(entryCalls);
expect(setZecPrice).not.toHaveBeenCalled();
});

test('opting in with the transport still off emits no clearnet fetch', async () => {
price.mockResolvedValue({ price: 42, error: '' });
const setZecPrice = jest.fn();
Expand Down
26 changes: 18 additions & 8 deletions components/Components/priceFetcherStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,31 @@ function transportRefuses(): boolean {
function surfaceMayFetch(): boolean {
return (
deps !== undefined &&
priceTrafficConsented() &&
priceTrafficAllowed() &&
deps.priceFetchable &&
attachCount > 0 &&
!appAway &&
!transportRefuses()
);
}

function priceTrafficConsented(): boolean {
return (
deps !== undefined &&
(deps.nymSelected
? deps.mixnetStatusKey !== 'mixnet.status.off'
: deps.mixnetStatusKey === 'mixnet.status.off')
);
function clearnetFetchable(key: MixnetStatusKey): boolean {
switch (key) {
case 'mixnet.status.off':
case 'mixnet.status.died':
case 'mixnet.status.unknown':
return true;
case 'mixnet.status.ready':
case 'mixnet.status.bootstrapping':
return false;
}
}

function priceTrafficAllowed(): boolean {
if (deps === undefined) return false;
return deps.nymSelected
? deps.mixnetStatusKey !== 'mixnet.status.off'
: clearnetFetchable(deps.mixnetStatusKey);
}

function scheduleAuto(): void {
Expand Down
Loading