Files
sousa-gecko/browser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js
T

707 lines
20 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { IPProtectionPanel } = ChromeUtils.importESModule(
"moz-src:///browser/components/ipprotection/IPProtectionPanel.sys.mjs"
);
const { IPProtectionServerlist } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/ipprotection/IPProtectionServerlist.sys.mjs"
);
/**
* A class that mocks the IP Protection panel.
*/
class FakeIPProtectionPanelElement {
constructor() {
this.state = {
isProtectionEnabled: false,
};
this.isConnected = false;
this.ownerDocument = {
removeEventListener() {
/* NOOP */
},
};
}
requestUpdate() {
/* NOOP */
}
remove() {
/* NOOP */
}
}
/**
* A class that mocks the IP Protection panel.
*/
class FakeIPProtectionPanelView {
constructor() {
this.state = "open";
this.ownerDocument = {
removeEventListener() {
/* NOOP */
},
};
}
hidePopup() {
/* NOOP */
}
}
add_setup(async function () {
// FxAccountsStorage.sys.mjs requires a profile directory.
do_get_profile();
await putServerInRemoteSettings();
await IPProtectionService.init();
registerCleanupFunction(async () => {
IPProtectionService.uninit();
});
});
/**
* Tests that we can set a state and pass it to a fake element.
*/
add_task(async function test_setState() {
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
ipProtectionPanel.state = {};
fakeElement.state = {};
ipProtectionPanel.setState({
foo: "bar",
});
Assert.deepEqual(
ipProtectionPanel.state,
{ foo: "bar" },
"The state should be set on the IPProtectionPanel instance"
);
Assert.deepEqual(
fakeElement.state,
{},
"The state should not be set on the fake element, as it is not connected"
);
fakeElement.isConnected = true;
ipProtectionPanel.setState({
isFoo: true,
});
Assert.deepEqual(
ipProtectionPanel.state,
{ foo: "bar", isFoo: true },
"The state should be set on the IPProtectionPanel instance"
);
Assert.deepEqual(
fakeElement.state,
{ foo: "bar", isFoo: true },
"The state should be set on the fake element"
);
});
/**
* Tests that the whole state will be updated when calling updateState directly.
*/
add_task(async function test_updateState() {
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
ipProtectionPanel.state = {};
fakeElement.state = {};
ipProtectionPanel.setState({
foo: "bar",
});
Assert.deepEqual(
fakeElement.state,
{},
"The state should not be set on the fake element, as it is not connected"
);
fakeElement.isConnected = true;
ipProtectionPanel.updateState();
Assert.deepEqual(
fakeElement.state,
{ foo: "bar" },
"The state should be set on the fake element"
);
});
/**
* Tests that we can set a state on multiple fake elements.
*/
add_task(async function test_updateComponentState() {
let ipProtectionPanel = new IPProtectionPanel();
let fakeElementA = new FakeIPProtectionPanelElement();
let fakeElementB = new FakeIPProtectionPanelElement();
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
ipProtectionPanel.state = {
foo: "bar",
};
fakeElementA.state = {};
fakeElementA.isConnected = true;
fakeElementB.state = {};
fakeElementB.isConnected = true;
ipProtectionPanel.updateComponentState(fakeElementA);
Assert.ok(
ipProtectionPanel.components.has(fakeElementA),
"The fake element A should be in the components set"
);
Assert.deepEqual(
fakeElementA.state,
{ foo: "bar" },
"The state should be set on the fake element A"
);
Assert.deepEqual(
fakeElementB.state,
{},
"The state should not be set on the fake element B"
);
ipProtectionPanel.updateComponentState(fakeElementB);
Assert.ok(
ipProtectionPanel.components.has(fakeElementB),
"The fake element B should be in the components set"
);
Assert.deepEqual(
fakeElementB.state,
{ foo: "bar" },
"The state should be set on the fake element B"
);
// Updating the state now should update both elements.
ipProtectionPanel.setState({
isFoo: true,
});
Assert.deepEqual(
fakeElementA.state,
{ foo: "bar", isFoo: true },
"The state should be set on the fake element A"
);
Assert.deepEqual(
fakeElementB.state,
{ foo: "bar", isFoo: true },
"The state should be set on the fake element B"
);
});
/**
* Tests that IPProtectionService ready state event updates the state.
*/
add_task(async function test_IPProtectionPanel_signedIn() {
IPPDummyAuthProvider.simulateSignIn(true);
IPPDummyAuthProvider.setEntitlement(createTestEntitlement(), {
silent: true,
});
IPPDummyAuthProvider.setGetEntitlementResponse({
entitlement: createTestEntitlement(),
});
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
fakeElement.isConnected = true;
let signedInEventPromise = waitForEvent(
IPProtectionService,
"IPProtectionService:StateChanged",
() => IPProtectionService.state === IPProtectionStates.READY
);
IPProtectionService.updateState();
await signedInEventPromise;
Assert.equal(
ipProtectionPanel.state.unauthenticated,
false,
"unauthenticated should be false in the IPProtectionPanel state"
);
Assert.equal(
fakeElement.state.unauthenticated,
false,
"unauthenticated should be false in the fake elements state"
);
});
/**
* Tests that IPProtectionService unauthenticated state event updates the state.
*/
add_task(async function test_IPProtectionPanel_signedOut() {
IPPDummyAuthProvider.simulateSignIn(false);
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
fakeElement.isConnected = true;
IPProtectionService.setState(IPProtectionStates.READY);
let signedOutEventPromise = waitForEvent(
IPProtectionService,
"IPProtectionService:StateChanged",
() => IPProtectionService.state === IPProtectionStates.UNAUTHENTICATED
);
IPProtectionService.updateState();
await signedOutEventPromise;
Assert.equal(
ipProtectionPanel.state.unauthenticated,
true,
"unauthenticated should be true in the IPProtectionPanel state"
);
Assert.equal(
fakeElement.state.unauthenticated,
true,
"unauthenticated should be true in the fake elements state"
);
});
/**
* Tests that start and stopping the IPProtectionService updates the state.
*/
add_task(async function test_IPProtectionPanel_started_stopped() {
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
fakeElement.isConnected = true;
IPPDummyAuthProvider.simulateSignIn(true);
IPPDummyAuthProvider.setEntitlement(createTestEntitlement(), {
silent: true,
});
IPPDummyAuthProvider.setGetEntitlementResponse({
entitlement: createTestEntitlement(),
});
IPPDummyAuthProvider.setProxyPass({
status: 200,
error: undefined,
pass: new ProxyPass(createProxyPassToken()),
usage: new ProxyUsage(
"5368709120",
"4294967296",
"2026-02-01T00:00:00.000Z"
),
});
IPProtectionService.updateState();
Assert.equal(
IPProtectionService.state,
IPProtectionStates.READY,
"IP Protection service should be in READY state before starting"
);
let startedEventPromise = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state === IPPProxyStates.ACTIVE
);
IPPProxyManager.start();
await startedEventPromise;
Assert.equal(
ipProtectionPanel.state.isProtectionEnabled,
true,
"isProtectionEnabled should be true in the IPProtectionPanel state"
);
Assert.equal(
fakeElement.state.isProtectionEnabled,
true,
"isProtectionEnabled should be true in the fake elements state"
);
let stoppedEventPromise = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
() => IPPProxyManager.state !== IPPProxyStates.ACTIVE
);
await IPPProxyManager.stop();
await stoppedEventPromise;
Assert.equal(
ipProtectionPanel.state.isProtectionEnabled,
false,
"isProtectionEnabled should be false in the IPProtectionPanel state"
);
Assert.equal(
fakeElement.state.isProtectionEnabled,
false,
"isProtectionEnabled should be false in the fake elements state"
);
});
/**
* Tests that locationsList is populated from IPProtectionServerlist and
* kept in sync with IPProtectionServerlist:ListChanged events.
*/
add_task(async function test_IPProtectionPanel_locationsList() {
await IPProtectionServerlist.maybeFetchList(true);
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
fakeElement.isConnected = true;
Assert.deepEqual(
ipProtectionPanel.state.locationsList,
IPProtectionServerlist.countries,
"locationsList should be set to IPProtectionServerlist.countries at construction"
);
Assert.ok(
ipProtectionPanel.state.locationsList.some(c => c.code === "US"),
"locationsList should include the seeded US country"
);
IPProtectionServerlist.dispatchEvent(
new Event("IPProtectionServerlist:ListChanged")
);
Assert.deepEqual(
ipProtectionPanel.state.locationsList,
IPProtectionServerlist.countries,
"locationsList should be refreshed when ListChanged fires"
);
Assert.deepEqual(
fakeElement.state.locationsList,
IPProtectionServerlist.countries,
"locationsList should propagate to the connected element"
);
ipProtectionPanel.uninit();
});
/**
* Tests that UsageChanged events with BigInt(0) remaining bandwidth
* are processed correctly (not treated as falsy and skipped).
*
* Regression test: BigInt(0) is falsy in JavaScript, so a guard like
* `!usage.remaining` would incorrectly bail out when remaining is exactly 0.
*/
add_task(async function test_IPProtectionPanel_usage_zero_remaining() {
setupStubs();
Services.prefs.setBoolPref("browser.ipProtection.bandwidth.enabled", true);
let ipProtectionPanel = new IPProtectionPanel();
let fakeElement = new FakeIPProtectionPanelElement();
ipProtectionPanel.components.add(fakeElement);
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
fakeElement.isConnected = true;
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
// Create a usage object with remaining = 0 (BigInt)
const usage = new ProxyUsage("5368709120", "0", "3026-02-01T00:00:00.000Z");
Assert.equal(usage.remaining, BigInt(0), "remaining should be BigInt(0)");
// Dispatch a UsageChanged event with zero remaining bandwidth
IPPProxyManager.dispatchEvent(
new CustomEvent("IPPProxyManager:UsageChanged", {
bubbles: true,
composed: true,
detail: { usage },
})
);
// With 0 bytes remaining out of 5GB, remainingPercent = 0.
// This is <= THIRD_THRESHOLD (0.1), so threshold should be set to 90.
const threshold = Services.prefs.getIntPref(
"browser.ipProtection.bandwidthThreshold",
0
);
Assert.equal(
threshold,
100,
"bandwidthThreshold pref should be 100 when remaining bandwidth is zero"
);
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled");
});
/**
* Tests that opening the panel while paused re-checks usage.
*/
add_task(async function test_showing_refreshes_usage_when_paused() {
let ipProtectionPanel = new IPProtectionPanel();
ipProtectionPanel.panel = new FakeIPProtectionPanelView();
let refreshUsageStub = sinon.stub(IPPProxyManager, "refreshUsage").resolves();
ipProtectionPanel.state.paused = false;
ipProtectionPanel.showing(ipProtectionPanel.panel);
Assert.ok(
refreshUsageStub.notCalled,
"refreshUsage should not be called when opening the panel while not paused"
);
ipProtectionPanel.state.paused = true;
ipProtectionPanel.showing(ipProtectionPanel.panel);
Assert.ok(
refreshUsageStub.calledOnce,
"refreshUsage should be called when opening the panel while paused"
);
refreshUsageStub.restore();
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.everOpenedPanel");
Services.prefs.clearUserPref("browser.ipProtection.openedPanelWithLocation");
});
/**
* Tests that showLocationButtonBadge is true when the dismissed pref is not set.
*/
add_task(async function test_location_badge_initial_state_pref_unset() {
Services.prefs.clearUserPref(
"browser.ipProtection.locationButtonBadgeDismissed"
);
let ipProtectionPanel = new IPProtectionPanel();
Assert.equal(
ipProtectionPanel.state.showLocationButtonBadge,
true,
"showLocationButtonBadge should be true when pref is not set"
);
ipProtectionPanel.uninit();
});
/**
* Tests that showLocationButtonBadge is false when the dismissed pref is set to true.
*/
add_task(async function test_location_badge_initial_state_pref_set() {
Services.prefs.setBoolPref(
"browser.ipProtection.locationButtonBadgeDismissed",
true
);
let ipProtectionPanel = new IPProtectionPanel();
Assert.equal(
ipProtectionPanel.state.showLocationButtonBadge,
false,
"showLocationButtonBadge should be false when pref is set to true"
);
ipProtectionPanel.uninit();
Services.prefs.clearUserPref(
"browser.ipProtection.locationButtonBadgeDismissed"
);
});
function dispatchUsageEvent(max, remaining) {
IPPProxyManager.dispatchEvent(
new CustomEvent("IPPProxyManager:UsageChanged", {
bubbles: true,
composed: true,
detail: {
usage: new ProxyUsage(
String(max),
String(remaining),
"3026-03-01T00:00:00.000Z"
),
},
})
);
}
/**
* Tests that bandwidth threshold telemetry events fire at 50%, 75%, and 90%.
*/
add_task(async function test_bandwidth_used_threshold_events() {
Services.fog.initializeFOG();
Services.fog.testResetFOG();
Services.prefs.setBoolPref("browser.ipProtection.bandwidth.enabled", true);
let ipProtectionPanel = new IPProtectionPanel();
// 40% used (60% remaining) - no thresholds crossed
dispatchUsageEvent(1000000, 600000);
Assert.equal(
Glean.ipprotection.bandwidthUsedThreshold.testGetValue(),
null,
"No threshold event should fire at 40% used"
);
// 55% used (45% remaining) - crosses 50%
dispatchUsageEvent(1000000, 450000);
let events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 1, "One threshold event should fire at 55% used");
Assert.equal(events[0].extra.percentage, "50", "Should report 50% threshold");
// 80% used (20% remaining) - crosses 75%
dispatchUsageEvent(1000000, 200000);
events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 2, "Two threshold events total at 80% used");
Assert.equal(events[1].extra.percentage, "75", "Should report 75% threshold");
// 95% used (5% remaining) - crosses 90%
dispatchUsageEvent(1000000, 50000);
events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 3, "Three threshold events total at 95% used");
Assert.equal(events[2].extra.percentage, "90", "Should report 90% threshold");
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled");
Services.fog.testResetFOG();
});
/**
* Tests that threshold events are not re-fired within the same usage period.
*/
add_task(async function test_bandwidth_thresholds_not_repeated_same_period() {
Services.fog.testResetFOG();
Services.prefs.setBoolPref("browser.ipProtection.bandwidth.enabled", true);
let ipProtectionPanel = new IPProtectionPanel();
// Cross 50% threshold
dispatchUsageEvent(1000000, 400000);
let events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 1, "One event after first call at 60% used");
// Same usage dispatched again - should not re-fire
dispatchUsageEvent(1000000, 400000);
events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(
events.length,
1,
"No additional event when threshold already reported"
);
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled");
Services.fog.testResetFOG();
});
/**
* Tests that thresholds reset when a new usage period begins.
*/
add_task(async function test_bandwidth_thresholds_reset_on_new_period() {
Services.fog.testResetFOG();
Services.prefs.setBoolPref("browser.ipProtection.bandwidth.enabled", true);
let ipProtectionPanel = new IPProtectionPanel();
// Cross 50% in the current period
dispatchUsageEvent(1000000, 400000);
let events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 1, "One event in current period");
Assert.equal(events[0].extra.percentage, "50");
// Simulate a period reset by returning to full bandwidth (threshold drops to 0)
dispatchUsageEvent(1000000, 1000000);
Services.fog.testResetFOG();
// 50% should fire again since the threshold pref was reset to 0
dispatchUsageEvent(1000000, 400000);
events = Glean.ipprotection.bandwidthUsedThreshold.testGetValue();
Assert.equal(events.length, 1, "50% fires again after period reset");
Assert.equal(events[0].extra.percentage, "50");
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled");
Services.fog.testResetFOG();
});
/**
* Tests that an unlimited UsageChanged event clears the bandwidth tracking
* prefs and resets the bandwidthUsage state.
*/
add_task(async function test_bandwidth_unlimited_usage_clears_tracking() {
Services.fog.initializeFOG();
Services.fog.testResetFOG();
Services.prefs.setBoolPref("browser.ipProtection.bandwidth.enabled", true);
Services.prefs.setIntPref("browser.ipProtection.bandwidthThreshold", 75);
Services.prefs.setStringPref(
"browser.ipProtection.bandwidthResetDate",
"3026-03-01T00:00:00.000Z"
);
let ipProtectionPanel = new IPProtectionPanel();
ipProtectionPanel.setState({
bandwidthUsage: { max: 1000000, remaining: 200000, reset: null },
});
IPPProxyManager.dispatchEvent(
new CustomEvent("IPPProxyManager:UsageChanged", {
bubbles: true,
composed: true,
detail: { usage: new ProxyUsage(null, null, null, true) },
})
);
Assert.strictEqual(
ipProtectionPanel.state.bandwidthUsage,
null,
"bandwidthUsage state should be reset for unlimited usage"
);
Assert.ok(
!Services.prefs.prefHasUserValue("browser.ipProtection.bandwidthThreshold"),
"bandwidthThreshold pref should be cleared for unlimited usage"
);
Assert.ok(
!Services.prefs.prefHasUserValue("browser.ipProtection.bandwidthResetDate"),
"bandwidthResetDate pref should be cleared for unlimited usage"
);
Assert.equal(
Glean.ipprotection.bandwidthUsedThreshold.testGetValue(),
null,
"No threshold telemetry should be recorded for unlimited usage"
);
ipProtectionPanel.uninit();
Services.prefs.clearUserPref("browser.ipProtection.bandwidth.enabled");
Services.prefs.clearUserPref("browser.ipProtection.bandwidthThreshold");
Services.prefs.clearUserPref("browser.ipProtection.bandwidthResetDate");
Services.fog.testResetFOG();
});