Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
const { IPPAlwaysOnSingleton } = ChromeUtils.importESModule(
);
const { IPPEarlyStartupFilter } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/ipprotection/IPPEarlyStartupFilter.sys.mjs"
);
const { IPProtectionServerlist, PrefServerList } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/ipprotection/IPProtectionServerlist.sys.mjs"
);
const TEST_SERVER = {
hostname: "proxy.example.com",
port: 443,
quarantined: false,
};
const TEST_COUNTRY = {
name: "United States",
code: "US",
cities: [{ name: "Test City", code: "TC", servers: [TEST_SERVER] }],
};
add_setup(async function () {
Services.prefs.setCharPref(
PrefServerList.PREF_NAME,
JSON.stringify([TEST_COUNTRY])
);
await IPProtectionServerlist.maybeFetchList();
registerCleanupFunction(() => {
Services.prefs.clearUserPref(PrefServerList.PREF_NAME);
});
});
/**
* Creates a fresh IPPAlwaysOnSingleton with alwaysOnEnabled stubbed.
*
* @param {object} sandbox - Sinon sandbox
* @param {object} [opts]
* @param {boolean} [opts.enabled=true] - Value for the alwaysOnEnabled getter
*/
function makeAlwaysOn(sandbox, { enabled = true } = {}) {
const alwaysOn = new IPPAlwaysOnSingleton();
sandbox.stub(alwaysOn, "alwaysOnEnabled").get(() => enabled);
return alwaysOn;
}
/**
* Registers `alwaysOn` as a helper so IPProtectionService initializes it after
* IPPProxyManager, ensuring IPPProxyManager is READY when alwaysOn first reacts
* to a service state change.
*
* @param {IPPAlwaysOnSingleton} alwaysOn
*/
function registerAsHelper(alwaysOn) {
IPProtectionActivator.addHelpers([alwaysOn]);
IPProtectionActivator.setupHelpers();
}
/**
* Restores the helper list to the state established in head_enterprise.js.
*/
function restoreHelpers() {
IPProtectionActivator.removeHelpers();
IPProtectionActivator.setupHelpers();
}
add_task(async function test_init_skipped_without_policy() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
// init() returns immediately when policy is absent, so ordering doesn't matter.
const alwaysOn = makeAlwaysOn(sandbox, { enabled: false });
alwaysOn.init();
const waitForReady = waitForEvent(
IPProtectionService,
"IPProtectionService:StateChanged",
() => IPProtectionService.state === IPProtectionStates.READY
);
IPProtectionService.init();
await waitForReady;
Assert.notEqual(
IPPProxyManager.state,
IPPProxyStates.ACTIVATING,
"Proxy should not start when the AccessConnector policy is absent"
);
IPProtectionService.uninit();
sandbox.restore();
});
add_task(async function test_proxy_starts_on_service_ready() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
Assert.equal(
IPPProxyManager.state,
IPPProxyStates.ACTIVE,
"Proxy should become active once the service is ready"
);
alwaysOn.uninit();
await IPPProxyManager.stop(false);
IPProtectionService.uninit();
restoreHelpers();
sandbox.restore();
});
add_task(async function test_proxy_starts_when_early_filter_marked_active() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
// Regression: IPPEarlyStartupFilter registering the channel filter causes
// IPPProxyManager to report ACTIVE before start() is called. #tryStart() must
// not bail in this case — it checks channelFilter()?.proxyInfo to distinguish
// a registered-but-uninitialised filter from a truly established connection.
const alwaysOn = makeAlwaysOn(sandbox);
const earlyFilter = new IPPEarlyStartupFilter(() => alwaysOn.alwaysOnEnabled);
IPProtectionActivator.addHelpers([alwaysOn, earlyFilter]);
IPProtectionActivator.setupHelpers();
// The premature ACTIVE fires first (filter registered, proxyInfo null).
// Wait for the second ACTIVE where proxyInfo is set.
const waitForTrulyActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() =>
IPPProxyManager.state === IPPProxyStates.ACTIVE &&
!!IPPProxyManager.channelFilter()?.proxyInfo
);
IPProtectionService.init();
await waitForTrulyActive;
Assert.equal(
IPPProxyManager.state,
IPPProxyStates.ACTIVE,
"Proxy should be ACTIVE"
);
Assert.ok(
IPPProxyManager.channelFilter()?.proxyInfo,
"proxyInfo should be set, not just the filter registered"
);
alwaysOn.uninit();
earlyFilter.uninit();
await IPPProxyManager.stop(false);
IPProtectionService.uninit();
restoreHelpers();
sandbox.restore();
});
add_task(async function test_proxy_restarts_on_unexpected_stop() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
// The proxy may advance past ACTIVATING to ACTIVE before the assertion runs
// since stubs are synchronous, so accept both.
const waitForRestart = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() =>
IPPProxyManager.state === IPPProxyStates.ACTIVATING ||
IPPProxyManager.state === IPPProxyStates.ACTIVE
);
await IPPProxyManager.stop(false);
await waitForRestart;
Assert.ok(
IPPProxyManager.state === IPPProxyStates.ACTIVATING ||
IPPProxyManager.state === IPPProxyStates.ACTIVE,
"Proxy should restart immediately after an unexpected stop"
);
// Uninit before stopping: with #pass cached, the restart path resolves faster
// and can race uninit(), causing a missing-activation-promise rejection.
alwaysOn.uninit();
await IPPProxyManager.stop(false);
IPProtectionService.uninit();
restoreHelpers();
sandbox.restore();
});
add_task(async function test_proxy_restarts_after_error() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
// Force an error — alwaysOn stops the proxy and restarts immediately.
const waitForRestart = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() =>
IPPProxyManager.state === IPPProxyStates.ACTIVATING ||
IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPPProxyManager.setErrorState(ERRORS.TIMEOUT);
await waitForRestart;
Assert.ok(
IPPProxyManager.state === IPPProxyStates.ACTIVATING ||
IPPProxyManager.state === IPPProxyStates.ACTIVE,
"Proxy should restart after entering an error state"
);
alwaysOn.uninit();
await IPPProxyManager.stop(false);
IPProtectionService.uninit();
restoreHelpers();
sandbox.restore();
});
add_task(async function test_proxy_not_restarted_during_policy_removal() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
// When the policy is removed, alwaysOnEnabled flips to false before uninit()
// runs. The subsequent ACTIVE->READY transition from teardown must not trigger
// a restart.
const alwaysOn = new IPPAlwaysOnSingleton();
let policyActive = true;
sandbox.stub(alwaysOn, "alwaysOnEnabled").get(() => policyActive);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
policyActive = false;
IPProtectionService.uninit();
Assert.notEqual(
IPPProxyManager.state,
IPPProxyStates.ACTIVATING,
"Proxy must not restart during the policy-removal uninit cascade"
);
restoreHelpers();
sandbox.restore();
});
add_task(async function test_proxy_not_restarted_when_service_unavailable() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
// The proxy may transiently re-enter ACTIVATING during uninit due to helper
// ordering; stop() drains it before we assert.
IPProtectionService.uninit();
await IPPProxyManager.stop(false);
Assert.notEqual(
IPPProxyManager.state,
IPPProxyStates.ACTIVATING,
"Proxy should not restart once the service becomes unavailable"
);
restoreHelpers();
sandbox.restore();
});
add_task(async function test_serverlist_change_calls_switch_when_active() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
sandbox.stub(IPPProxyManager, "switch").returns({ error: null });
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
// The pref observer registered in initOnStartupCompleted fires synchronously.
const updatedServer = {
hostname: "proxy2.example.com",
port: 443,
quarantined: false,
};
const updatedCountry = {
...TEST_COUNTRY,
cities: [{ name: "Test City", code: "TC", servers: [updatedServer] }],
};
Services.prefs.setCharPref(
PrefServerList.PREF_NAME,
JSON.stringify([updatedCountry])
);
Assert.ok(
IPPProxyManager.switch.calledOnce,
"switch() should be called when the serverlist changes while the proxy is active"
);
alwaysOn.uninit();
await IPPProxyManager.stop(false);
IPProtectionService.uninit();
restoreHelpers();
sandbox.restore();
});
add_task(async function test_serverlist_cleared_stops_proxy() {
const sandbox = sinon.createSandbox();
setupStubs(sandbox);
const alwaysOn = makeAlwaysOn(sandbox);
registerAsHelper(alwaysOn);
const waitForActive = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPProtectionService.init();
await waitForActive;
const waitForReady = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.READY
);
Services.prefs.clearUserPref(PrefServerList.PREF_NAME);
await waitForReady;
Assert.equal(
IPPProxyManager.state,
IPPProxyStates.READY,
"Proxy should stop when the serverlist is cleared"
);
Assert.notEqual(
IPPProxyManager.state,
IPPProxyStates.ACTIVATING,
"Proxy should not attempt to restart with an empty serverlist"
);
IPProtectionService.uninit();
restoreHelpers();
Services.prefs.setCharPref(
PrefServerList.PREF_NAME,
JSON.stringify([TEST_COUNTRY])
);
await IPProtectionServerlist.maybeFetchList(true);
sandbox.restore();
});