/* Any copyright is dedicated to the Public Domain. https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { IPProtectionServerlist, PrefServerList, RemoteSettingsServerlist, IPProtectionServerlistFactory, } = ChromeUtils.importESModule( "moz-src:///toolkit/components/ipprotection/IPProtectionServerlist.sys.mjs" ); const COLLECTION_NAME = "vpn-serverlist"; const TEST_SERVER_1 = { hostname: "test1.example.com", port: 443, quarantined: false, protocols: [ { name: "connect", host: "test1.example.com", port: 8443, scheme: "https", }, ], }; const TEST_SERVER_2 = { hostname: "test2.example.com", port: 443, quarantined: false, protocols: [ { name: "connect", host: "test2.example.com", port: 8443, scheme: "https", }, ], }; const TEST_SERVER_QUARANTINED = { hostname: "quarantined.example.com", port: 443, quarantined: true, protocols: [ { name: "connect", host: "quarantined.example.com", port: 8443, scheme: "https", }, ], }; const TEST_US_CITY = { name: "Test City", code: "TC", servers: [TEST_SERVER_1, TEST_SERVER_2], }; const TEST_REC_CITY = { name: "Anycast", code: "REC1", servers: [TEST_SERVER_1], }; const TEST_QUARANTINED_CITY = { name: "All Quarantined", code: "QC1", servers: [TEST_SERVER_QUARANTINED], }; const TEST_REC_COUNTRY = { name: "Recommended", code: "REC", cities: [TEST_REC_CITY], }; const TEST_COUNTRIES = [ TEST_REC_COUNTRY, { name: "United States", code: "US", locked: false, cities: [TEST_US_CITY], }, { name: "Canada", code: "CA", locked: false, cities: [ { name: "Test City 2", code: "TC2", servers: [TEST_SERVER_1], }, ], }, { name: "Quarantineland", code: "QL", locked: false, cities: [TEST_QUARANTINED_CITY], }, { name: "Locksmithania", code: "LK", locked: true, cities: [ { name: "Vault", code: "VLT", servers: [TEST_SERVER_1], }, ], }, ]; const client = RemoteSettings(COLLECTION_NAME); add_setup(async function () { do_get_profile(); await client.db.clear(); for (const country of TEST_COUNTRIES) { await client.db.create(country); } await client.db.importChanges({}, Date.now()); await IPProtectionServerlist.maybeFetchList(); await IPProtectionServerlist.initOnStartupCompleted(); Assert.ok(IPProtectionServerlist instanceof RemoteSettingsServerlist); }); add_task(async function test_getRecommendedLocation() { const rec = IPProtectionServerlist.getRecommendedLocation(); Assert.equal(rec.country.code, "REC", "Recommended country is REC"); Assert.deepEqual(rec.city, TEST_REC_CITY, "Recommended city is the REC city"); const defaultArg = IPProtectionServerlist.getLocation(); Assert.deepEqual( defaultArg, rec, "getLocation() with no argument returns the recommended location" ); const byRecCode = IPProtectionServerlist.getLocation("REC"); Assert.deepEqual( byRecCode, rec, "getLocation('REC') returns the recommended location" ); }); add_task(async function test_getRecommendedLocation_fallback() { const withoutRec = TEST_COUNTRIES.filter(c => c.code !== "REC"); Services.prefs.setCharPref( PrefServerList.PREF_NAME, JSON.stringify(withoutRec) ); try { const fallbackList = new PrefServerList(); await fallbackList.maybeFetchList(); Assert.equal( fallbackList.getLocation("REC"), null, "Without a REC entry, getLocation('REC') is null" ); const rec = fallbackList.getRecommendedLocation(); Assert.equal( rec.country.code, "US", "Recommended falls back to US when REC is absent" ); Assert.deepEqual(rec.city, TEST_US_CITY, "Fallback city is the US city"); } finally { Services.prefs.clearUserPref(PrefServerList.PREF_NAME); } }); add_task(async function test_getLocation_byCode() { const us = IPProtectionServerlist.getLocation("US"); Assert.equal(us.country.code, "US", "getLocation('US') returns the US entry"); Assert.deepEqual(us.city, TEST_US_CITY, "getLocation('US') returns US city"); Assert.equal( IPProtectionServerlist.getLocation("ZZ"), null, "Unknown country codes return null" ); // Countries with only quarantined servers still return a {country, city} // (quarantine is enforced later in selectServer); verify selectServer // filters it out. const ql = IPProtectionServerlist.getLocation("QL"); Assert.equal(ql.country.code, "QL", "getLocation('QL') returns the QL entry"); Assert.equal( IPProtectionServerlist.selectServer(ql.city), null, "selectServer returns null for a city with only quarantined servers" ); }); add_task(async function test_countries() { const countries = IPProtectionServerlist.countries; const codes = countries.map(c => c.code); Assert.ok(!codes.includes("REC"), "REC is excluded from the countries list"); Assert.deepEqual( codes.sort(), ["CA", "LK", "QL", "US"], "Every non-REC country is included" ); const byCode = Object.fromEntries(countries.map(c => [c.code, c])); Assert.equal(byCode.US.available, true, "US has a non-quarantined server"); Assert.equal(byCode.CA.available, true, "CA has a non-quarantined server"); Assert.equal( byCode.QL.available, false, "QL has only quarantined servers, so available is false" ); ["US", "CA", "QL"].forEach(code => { Assert.equal( byCode[code].locked, false, `${code} has \`locked: false\` in remote-settings and is parsed correctly` ); }); Assert.equal( byCode.LK.locked, true, "LK has `locked: true` in remote-settings and is parsed correctly" ); }); add_task(async function test_country_locked_parsing() { Assert.equal( IPProtectionServerlist.getLocation("LK").country.locked, true, "getLocation('LK').country.locked is true" ); Assert.equal( IPProtectionServerlist.getLocation("US").country.locked, false, "getLocation('US').country.locked is false" ); Assert.equal( IPProtectionServerlist.getLocation("REC").country.locked, false, "getLocation('REC').country.locked defaults to false" ); }); add_task(async function test_listChangedEvent() { let fired = 0; const onChanged = () => { fired++; }; IPProtectionServerlist.addEventListener( "IPProtectionServerlist:ListChanged", onChanged ); try { await client.emit("sync", { data: {} }); Assert.greater( fired, 0, "ListChanged event is dispatched when RS sync triggers a refetch" ); } finally { IPProtectionServerlist.removeEventListener( "IPProtectionServerlist:ListChanged", onChanged ); } }); add_task(async function test_selectServer() { // Test with a city with multiple non-quarantined servers let selected = IPProtectionServerlist.selectServer(TEST_US_CITY); Assert.ok( [TEST_SERVER_1, TEST_SERVER_2].some(s => s.hostname === selected.hostname), "A valid server should be selected" ); // Test with a city with one server const cityWithOneServer = { name: "One Server City", code: "OSC", servers: [TEST_SERVER_1], }; selected = IPProtectionServerlist.selectServer(cityWithOneServer); Assert.deepEqual( selected, TEST_SERVER_1, "The single server should be selected" ); // Test with a city with a mix of quarantined and non-quarantined servers const cityWithMixedServers = { name: "Mixed Servers City", code: "MSC", servers: [TEST_SERVER_1, TEST_SERVER_QUARANTINED], }; selected = IPProtectionServerlist.selectServer(cityWithMixedServers); Assert.deepEqual( selected, TEST_SERVER_1, "The non-quarantined server should be selected" ); // Test with a city with only quarantined servers const cityWithQuarantinedServers = { name: "Quarantined City", code: "QC", servers: [TEST_SERVER_QUARANTINED], }; selected = IPProtectionServerlist.selectServer(cityWithQuarantinedServers); Assert.equal(selected, null, "No server should be selected"); // Test with a city with no servers const cityWithNoServers = { name: "No Server City", code: "NSC", servers: [], }; selected = IPProtectionServerlist.selectServer(cityWithNoServers); Assert.equal(selected, null, "No server should be selected"); }); add_task(async function test_syncRespected() { let { country, city } = IPProtectionServerlist.getLocation("US"); Assert.equal(country.code, "US", "getLocation('US') returns the US entry"); Assert.deepEqual(city, TEST_US_CITY, "The correct city should be returned"); // Now, update the server list: keep only an updated US entry. const updated_server = { ...TEST_SERVER_1, hostname: "updated.example.com", }; const updated_city = { ...TEST_US_CITY, servers: [updated_server], }; const updated_country = { name: "United States", code: "US", cities: [updated_city], }; await client.db.clear(); await client.db.create(updated_country); await client.db.importChanges({}, Date.now()); await client.emit("sync", { data: {} }); await IPProtectionServerlist.maybeFetchList(); ({ country, city } = IPProtectionServerlist.getLocation("US")); Assert.equal(country.code, "US", "getLocation('US') still returns US"); Assert.deepEqual(city, updated_city, "The updated city should be returned"); }); add_task(async function test_PrefServerList() { registerCleanupFunction(() => { Services.prefs.clearUserPref(PrefServerList.PREF_NAME); }); Services.prefs.setCharPref( PrefServerList.PREF_NAME, JSON.stringify(TEST_COUNTRIES) ); Assert.equal( PrefServerList.hasPrefValue, true, "PrefServerList should have a pref value set." ); Assert.deepEqual( PrefServerList.prefValue, TEST_COUNTRIES, "PrefServerList's pref value should match the set value." ); const serverList = new PrefServerList(); await serverList.maybeFetchList(); const { country, city } = serverList.getLocation("US"); Assert.equal(country.code, "US", "getLocation('US') returns the US entry"); Assert.deepEqual(city, TEST_US_CITY, "The US city should be returned."); }); add_task(async function test_PrefServerList_prefChangeTriggersListChanged() { registerCleanupFunction(() => { Services.prefs.clearUserPref(PrefServerList.PREF_NAME); }); Services.prefs.setCharPref( PrefServerList.PREF_NAME, JSON.stringify(TEST_COUNTRIES) ); const serverList = new PrefServerList(); await serverList.initOnStartupCompleted(); Assert.ok(serverList.hasList, "Initial list should be loaded."); let listChangedFired = false; serverList.addEventListener("IPProtectionServerlist:ListChanged", () => { listChangedFired = true; }); const updatedList = [ { code: "US", cities: [{ servers: [{ host: "updated.example.com", port: "9090" }] }], }, ]; Services.prefs.setCharPref( PrefServerList.PREF_NAME, JSON.stringify(updatedList) ); Assert.ok( listChangedFired, "ListChanged should fire when the serverlist pref changes." ); serverList.uninit(); }); add_task(async function test_IPProtectionServerlistFactory() { registerCleanupFunction(() => { Services.prefs.clearUserPref(PrefServerList.PREF_NAME); }); // Without the pref set, it should return RemoteSettingsServerlist Services.prefs.clearUserPref(PrefServerList.PREF_NAME); let instance = IPProtectionServerlistFactory(); Assert.ok(instance instanceof RemoteSettingsServerlist); Services.prefs.setCharPref( PrefServerList.PREF_NAME, JSON.stringify(TEST_COUNTRIES) ); // With the pref set, it should return PrefServerList Assert.ok(IPProtectionServerlistFactory() instanceof PrefServerList); });