/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { HttpServer } = ChromeUtils.importESModule( "resource://testing-common/httpd.sys.mjs" ); const { DAPReportController, Task } = ChromeUtils.importESModule( "resource://gre/modules/DAPReportController.sys.mjs" ); const BinaryInputStream = Components.Constructor( "@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream" ); const PREF_LEADER = "toolkit.telemetry.dap.leader.url"; const PREF_HELPER = "toolkit.telemetry.dap.helper.url"; let server; let server_addr; // The dummy test server will record report sizes in this list. let server_requests = []; function uploadHandler(request, response) { Assert.equal( request.getHeader("Content-Type"), "application/dap-report", "Wrong Content-Type header." ); let body = new BinaryInputStream(request.bodyInputStream); server_requests.push(body.available()); response.setStatusLine(request.httpVersion, 200); } add_setup(async function () { do_get_profile(); // Set up a mock server to represent the DAP endpoints. server = new HttpServer(); server.registerPrefixHandler("/leader_endpoint/tasks/", uploadHandler); server.start(-1); const i = server.identity; server_addr = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; Services.prefs.setStringPref(PREF_LEADER, server_addr + "/leader_endpoint"); Services.prefs.setStringPref(PREF_HELPER, server_addr + "/helper_endpoint"); registerCleanupFunction(() => { Services.prefs.clearUserPref(PREF_LEADER); Services.prefs.clearUserPref(PREF_HELPER); return new Promise(resolve => { server.stop(resolve); }); }); }); function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open("SubmissionCap", 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function countRecords(store) { return new Promise((resolve, reject) => { const request = store.count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function getReportCount(db) { try { const tx = db.transaction("reports", "readonly"); const store = tx.objectStore("reports"); const count = await countRecords(store); await tx.done; return count; } catch (err) { if (err.name === "NotFoundError") { console.error("Object store 'reports' not found"); } else { console.error("IndexedDB access error:", err); } // Cannot return 0 since 0 is a valid result. return Number.MAX_SAFE_INTEGER; } } async function getFreqCapCount(db) { try { const tx = db.transaction("freq_caps", "readonly"); const store = tx.objectStore("freq_caps"); const count = await countRecords(store); await tx.done; return count; } catch (err) { if (err.name === "NotFoundError") { console.error("Object store 'freq_caps' not found"); } else { console.error("IndexedDB access error:", err); } // Cannot return 0 since 0 is a valid result. return Number.MAX_SAFE_INTEGER; } } add_task(async function testSupportedVdafs() { server_requests = []; const histogram_task = new Task({ taskId: "QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8", vdaf: "histogram", length: 30, timePrecision: 60, defaultMeasurement: 0, }); const sumvec_task = new Task({ taskId: "52TTU9GPOA_eTiPJePk5RNauQI4EWCnzixAXe3LEz7o", vdaf: "sumvec", length: 20, bits: 8, timePrecision: 60, defaultMeasurement: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }); const sum_task = new Task({ taskId: "RnywY1X4s1vtspu6B8C1FOu_jJZhJO6V8L3PT3WepF4", vdaf: "sum", bits: 8, timePrecision: 60, defaultMeasurement: 0, }); let tasks = {}; tasks[histogram_task._taskId] = histogram_task; tasks[sumvec_task._taskId] = sumvec_task; tasks[sum_task._taskId] = sum_task; let fakeTimestamp = new Date("1970-01-01T00:00:00Z").getTime(); let dapReportContoller = new DAPReportController({ tasks, options: { windowDays: 7, submissionIntervalMins: 240, }, DateNowFn: () => fakeTimestamp, }); await dapReportContoller.startTimedSubmission(); // The detailed test validations are only performed for histogram_task, // The other tasks verify the submission of the default measurement. // Initial state validation const db = await openDatabase(); let numRecords = await getReportCount(db); Assert.equal(numRecords, 0, "Should be no pending reports"); // Record a measurment and validate pending report await dapReportContoller.recordMeasurement(histogram_task._taskId, 1); numRecords = await getReportCount(db); Assert.equal(numRecords, 1, "Should be 1 pending report"); let report = await dapReportContoller.getReportToSubmit( histogram_task._taskId ); let expectedReport = { taskId: histogram_task._taskId, vdaf: histogram_task._vdaf, bits: undefined, length: histogram_task._length, measurement: 1, }; Assert.deepEqual( report, expectedReport, "Pending report should match recorded task and measurement" ); // Submit the measurement await dapReportContoller.submit(1000, "unit-test"); // State validation numRecords = await getReportCount(db); Assert.equal(numRecords, 0, "Should be 0 pending reports after submit"); // Verify submission capping is active numRecords = await getFreqCapCount(db); Assert.equal( numRecords, 1, "Frequency capping should indicate a report has been submitted" ); let freqCap = await dapReportContoller.getFreqCap(histogram_task._taskId); let expectedFreqCap = { taskId: histogram_task._taskId, nextReset: fakeTimestamp + 7 * 24 * 60 * 60 * 1000, }; Assert.deepEqual( freqCap, expectedFreqCap, "Cap should indicate nextReset time." ); // Record another measurement await dapReportContoller.recordMeasurement(histogram_task._taskId, 2); numRecords = await getReportCount(db); Assert.equal( numRecords, 0, "Should be 0 pending reports due to submission with freq cap window" ); // Advance now() by more than windowDays ( 7 days + 1 ms) fakeTimestamp = fakeTimestamp + 7 * 24 * 60 * 60 * 1000 + 1; // Record another measurement and validate pending report await dapReportContoller.recordMeasurement(histogram_task._taskId, 3); numRecords = await getReportCount(db); Assert.equal(numRecords, 1, "Should be 1 pending report"); report = await dapReportContoller.getReportToSubmit(histogram_task._taskId); expectedReport = { taskId: histogram_task._taskId, vdaf: histogram_task._vdaf, bits: undefined, length: histogram_task._length, measurement: 3, }; Assert.deepEqual( report, expectedReport, "Pending report should match recorded task and measurement" ); // Submit the measurement await dapReportContoller.submit(1000, "unit-test"); numRecords = await getReportCount(db); Assert.equal( numRecords, 0, "Should be 0 pending reports since now() is beyond the freq cap window" ); // Verify submission capping is active numRecords = await getFreqCapCount(db); Assert.equal(numRecords, 1, "Should be 1 cap entry"); freqCap = await dapReportContoller.getFreqCap(histogram_task._taskId); expectedFreqCap = { taskId: histogram_task._taskId, nextReset: fakeTimestamp + 7 * 24 * 60 * 60 * 1000, }; Assert.deepEqual( freqCap, expectedFreqCap, "Cap should indicate nextReset time." ); // Stop submission timer and cleanup. await dapReportContoller.cleanup(30 * 1000, "stop collector"); //Verify IndexDB Cleanup after unenrollment numRecords = await getFreqCapCount(db); Assert.equal(numRecords, 0, "Should be 0 freq cap entries after cleanup"); //Report numRecords = await getReportCount(db); Assert.equal(numRecords, 0, "Should be 0 pending reports after cleanup"); // There are 4 server requests per task (the sizes match the vdaf used): // 1. The first request is submitted on enrollment // 2. The second request is force submitted // 3. The third request is force submitted // 4. The last request is submitted on unenrollment Assert.deepEqual( server_requests.sort((a, b) => a - b), [886, 886, 886, 886, 1126, 1126, 1126, 1126, 3654, 3654, 3654, 3654], "Should have one report on dapReportContoller create, second for recorded measurement, third on cleanup" ); }); add_task(async function testTaskMetadata() { const task = new Task({ taskId: "task_id1", vdaf: "sumvec", length: 25, timePrecision: 3600, defaultMeasurement: 1, }); Assert.equal(task._taskId, "task_id1", "taskId not correctly set"); Assert.equal(task._vdaf, "sumvec", "vdaf not correctly set"); Assert.equal(task._length, 25, "length not correctly set"); Assert.equal(task._timePrecision, 3600, "timePrecision not correctly set"); Assert.equal( task._defaultMeasurement, 1, "defaultMeasurement not correctly set" ); });