test_viaduct_necko_backend.js (11174B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* global add_setup, add_task, Assert, info, do_get_profile, do_timeout, registerCleanupFunction, Services */ 8 9 const { HttpServer } = ChromeUtils.importESModule( 10 "resource://testing-common/httpd.sys.mjs" 11 ); 12 13 const { NetUtil } = ChromeUtils.importESModule( 14 "resource://gre/modules/NetUtil.sys.mjs" 15 ); 16 17 // Create a test HTTP server 18 let gHttpServer = null; 19 let gServerPort = -1; 20 let gServerURL = ""; 21 22 // Track requests received by the server 23 let gRequestsReceived = []; 24 25 /** 26 * Helper to decode potentially gzipped request body 27 */ 28 function decodeRequestBody(request) { 29 let bodyStream = request.bodyInputStream; 30 let avail = bodyStream.available(); 31 if (avail === 0) { 32 return ""; 33 } 34 35 if ( 36 request.hasHeader("content-encoding") && 37 request.getHeader("content-encoding") === "gzip" 38 ) { 39 return "[gzipped content]"; 40 } 41 42 return NetUtil.readInputStreamToString(bodyStream, avail); 43 } 44 45 /** 46 * Wait for requests to arrive and settle. 47 * 48 * This waits for at least one request to arrive, then continues waiting 49 * until no new requests arrive for `settleTime` milliseconds. This ensures 50 * all pending viaduct-necko operations complete before the test proceeds, 51 * avoiding race conditions where background requests might still be in flight. 52 */ 53 async function waitForRequestsToSettle( 54 settleTime = 300, 55 timeout = 10000, 56 interval = 50 57 ) { 58 let start = Date.now(); 59 60 // First, wait for at least one request to arrive 61 while (gRequestsReceived.length === 0) { 62 if (Date.now() - start > timeout) { 63 throw new Error("Timeout waiting for initial request"); 64 } 65 await new Promise(resolve => do_timeout(interval, resolve)); 66 } 67 68 // Now wait for requests to settle (no new requests for settleTime ms) 69 let lastCount = 0; 70 let lastChangeTime = Date.now(); 71 72 while (Date.now() - lastChangeTime < settleTime) { 73 if (Date.now() - start > timeout) { 74 info( 75 `Timeout waiting for requests to settle, proceeding with ${gRequestsReceived.length} requests` 76 ); 77 break; 78 } 79 80 if (gRequestsReceived.length !== lastCount) { 81 lastCount = gRequestsReceived.length; 82 lastChangeTime = Date.now(); 83 } 84 85 await new Promise(resolve => do_timeout(interval, resolve)); 86 } 87 88 info(`Requests settled with ${gRequestsReceived.length} total requests`); 89 } 90 91 /** 92 * Setup function to initialize the HTTP server 93 */ 94 add_setup(async function () { 95 info("Setting up viaduct-necko test environment"); 96 97 // FOG needs a profile directory to store its data 98 do_get_profile(); 99 100 // Create and start the HTTP server 101 gHttpServer = new HttpServer(); 102 103 // Register various test endpoints 104 setupTestEndpoints(); 105 106 gHttpServer.start(-1); 107 gServerPort = gHttpServer.identity.primaryPort; 108 gServerURL = `http://localhost:${gServerPort}`; 109 110 info(`Test HTTP server started on port ${gServerPort}`); 111 112 // Set the telemetry port preference to use our test server 113 Services.prefs.setIntPref("telemetry.fog.test.localhost_port", gServerPort); 114 115 // Enable telemetry upload (needed for viaduct to be used) 116 Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true); 117 118 // Initialize FOG/Glean which should trigger viaduct-necko initialization 119 // This internally calls init_necko_backend() through GkRust_Init 120 Services.fog.testResetFOG(); 121 122 registerCleanupFunction(async () => { 123 Services.prefs.clearUserPref("telemetry.fog.test.localhost_port"); 124 Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled"); 125 await new Promise(resolve => gHttpServer.stop(resolve)); 126 }); 127 }); 128 129 /** 130 * Setup test endpoints on the HTTP server 131 */ 132 function setupTestEndpoints() { 133 // Glean telemetry submission endpoints 134 gHttpServer.registerPrefixHandler("/submit/", (request, response) => { 135 const path = request.path; 136 info(`Viaduct request received: ${request.method} ${path}`); 137 138 // Read the request body 139 const body = decodeRequestBody(request); 140 141 // Extract ping type from path 142 // Path format: /submit/firefox-desktop/{ping-type}/1/{uuid} 143 const pathParts = path.split("/"); 144 const pingType = pathParts[3] || "unknown"; 145 146 gRequestsReceived.push({ 147 path, 148 method: request.method, 149 pingType, 150 body, 151 bodySize: request.hasHeader("content-length") 152 ? parseInt(request.getHeader("content-length"), 10) 153 : body.length, 154 headers: { 155 "content-type": request.hasHeader("content-type") 156 ? request.getHeader("content-type") 157 : null, 158 "content-encoding": request.hasHeader("content-encoding") 159 ? request.getHeader("content-encoding") 160 : null, 161 "user-agent": request.hasHeader("user-agent") 162 ? request.getHeader("user-agent") 163 : null, 164 }, 165 }); 166 167 // Return a response similar to what a telemetry server would return 168 response.setStatusLine(request.httpVersion, 200, "OK"); 169 response.setHeader("Server", "TestServer/1.0 (Viaduct Backend)"); 170 response.setHeader("Date", new Date().toUTCString()); 171 response.setHeader("Connection", "close"); 172 response.setHeader("Content-Type", "application/json"); 173 174 const responseBody = JSON.stringify({ 175 status: "ok", 176 message: "Test server response", 177 }); 178 response.setHeader("Content-Length", responseBody.length.toString()); 179 response.write(responseBody); 180 }); 181 } 182 183 /** 184 * Test that the viaduct-necko backend was initialized and is processing requests 185 */ 186 add_task(async function test_viaduct_backend_working() { 187 info("Testing viaduct-necko backend initialization and request processing"); 188 189 const initialRequestCount = gRequestsReceived.length; 190 info( 191 `Already received ${initialRequestCount} requests during initialization` 192 ); 193 194 // Wait for requests to arrive AND settle. This is critical to avoid race 195 // conditions where background viaduct-necko operations might still be 196 // completing when subsequent tests run. 197 await waitForRequestsToSettle(); 198 199 // Check that we've received requests through viaduct 200 Assert.ok( 201 !!gRequestsReceived.length, 202 `Viaduct-necko backend is processing requests. Received ${gRequestsReceived.length} requests.` 203 ); 204 205 // Verify the requests are health pings 206 const healthPings = gRequestsReceived.filter(r => r.pingType === "health"); 207 info(`Received ${healthPings.length} health pings through viaduct-necko`); 208 209 // All telemetry submissions should be POST requests 210 for (const request of gRequestsReceived) { 211 Assert.equal( 212 request.method, 213 "POST", 214 `Request to ${request.path} should be POST` 215 ); 216 } 217 218 // Log summary of what was processed 219 const pingTypes = [...new Set(gRequestsReceived.map(r => r.pingType))]; 220 info( 221 `Test successful: Viaduct-necko backend processed ${gRequestsReceived.length} requests` 222 ); 223 info(`Ping types received: ${pingTypes.join(", ")}`); 224 info( 225 `The C++ backend successfully handled requests from Rust through the FFI layer` 226 ); 227 }); 228 229 /** 230 * Test different HTTP parameters and methods. 231 * We verify different body sizes and headers are handled correctly. 232 * 233 * Note: We don't call testResetFOG() again here because it can hang in chaos 234 * mode due to viaduct's synchronous waiting pattern combined with main-thread 235 * dispatch. Instead, we analyze the requests already collected from setup. 236 */ 237 add_task(async function test_different_parameters() { 238 info("Testing different HTTP parameters through viaduct-necko"); 239 240 const requestCount = gRequestsReceived.length; 241 info(`Analyzing ${requestCount} requests from previous operations`); 242 243 // Verify different content types and encodings were handled 244 const contentTypes = new Set(); 245 const contentEncodings = new Set(); 246 const bodySizes = new Set(); 247 248 for (const request of gRequestsReceived) { 249 if (request.headers["content-type"]) { 250 contentTypes.add(request.headers["content-type"]); 251 } 252 if (request.headers["content-encoding"]) { 253 contentEncodings.add(request.headers["content-encoding"]); 254 } 255 if (request.bodySize) { 256 bodySizes.add(request.bodySize); 257 } 258 } 259 260 info(`Content types seen: ${Array.from(contentTypes).join(", ")}`); 261 info(`Content encodings seen: ${Array.from(contentEncodings).join(", ")}`); 262 info( 263 `Body sizes seen: ${Array.from(bodySizes) 264 .sort((a, b) => a - b) 265 .join(", ")}` 266 ); 267 268 Assert.ok( 269 !!gRequestsReceived.length, 270 "Different parameters were processed successfully" 271 ); 272 273 // Verify we're seeing body sizes (at least one request with a body) 274 Assert.ok( 275 bodySizes.size >= 1, 276 `Body sizes handled: ${Array.from(bodySizes).join(", ")}` 277 ); 278 }); 279 280 /** 281 * Test that headers are properly passed through the FFI layer 282 */ 283 add_task(async function test_header_handling() { 284 info("Testing header handling through viaduct-necko"); 285 286 // Check the headers that were sent in previous requests 287 let hasHeaders = false; 288 let headerCount = 0; 289 290 for (const request of gRequestsReceived) { 291 if (request.headers && Object.keys(request.headers).length) { 292 hasHeaders = true; 293 const nonNullHeaders = Object.entries(request.headers).filter( 294 ([, value]) => value !== null 295 ); 296 297 if (nonNullHeaders.length) { 298 headerCount++; 299 info( 300 `Request headers found: ${JSON.stringify(Object.fromEntries(nonNullHeaders))}` 301 ); 302 } 303 } 304 } 305 306 Assert.ok( 307 hasHeaders, 308 "Headers are properly transmitted through viaduct-necko" 309 ); 310 311 Assert.ok(headerCount > 0, `Found ${headerCount} requests with headers`); 312 313 // Verify specific headers we expect to see 314 const hasContentType = gRequestsReceived.some( 315 r => r.headers && r.headers["content-type"] !== null 316 ); 317 const hasContentEncoding = gRequestsReceived.some( 318 r => r.headers && r.headers["content-encoding"] !== null 319 ); 320 const hasUserAgent = gRequestsReceived.some( 321 r => r.headers && r.headers["user-agent"] !== null 322 ); 323 324 Assert.ok(hasContentType, "Content-Type header is present"); 325 Assert.ok(hasContentEncoding, "Content-Encoding header is present"); 326 Assert.ok(hasUserAgent, "User-Agent header is present"); 327 328 info("Headers are properly handled through the Rust -> C++ -> Necko chain"); 329 }); 330 331 /** 332 * Test configuration validation. 333 * We verify that the configuration is being passed correctly by checking 334 * that requests complete successfully. 335 */ 336 add_task(async function test_configuration_validation() { 337 info("Validating viaduct-necko configuration"); 338 339 // We can verify at least that requests are completing successfully 340 // which means the configuration isn't breaking anything 341 const successfulRequests = gRequestsReceived.filter( 342 r => r.method === "POST" && r.path.includes("/submit/") 343 ); 344 345 Assert.ok( 346 !!successfulRequests.length, 347 `Configuration is valid: ${successfulRequests.length} successful requests processed` 348 ); 349 350 info( 351 "Viaduct-necko backend configuration validated through successful request processing" 352 ); 353 });