test_cache_behavior.js (7248B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { ObliviousHTTP } = ChromeUtils.importESModule( 7 "resource://gre/modules/ObliviousHTTP.sys.mjs" 8 ); 9 const { sinon } = ChromeUtils.importESModule( 10 "resource://testing-common/Sinon.sys.mjs" 11 ); 12 const { HttpServer } = ChromeUtils.importESModule( 13 "resource://testing-common/httpd.sys.mjs" 14 ); 15 const { TestUtils } = ChromeUtils.importESModule( 16 "resource://testing-common/TestUtils.sys.mjs" 17 ); 18 19 let gHttpServer; 20 21 const TEST_CONTENT_TYPE = "image/jpeg"; 22 const TEST_CONTENT_CHARSET = "UTF-8"; 23 const TEST_CONTENT_TYPE_HEADER = `${TEST_CONTENT_TYPE};charset=${TEST_CONTENT_CHARSET}`; 24 25 /** 26 * Waits for an nsICacheEntry to exist for urlString in the anonymous load 27 * context with > 0 data size. 28 * 29 * @param {string} urlString 30 * The string of the URL to check for. 31 * @returns {Promise<undefined>} 32 */ 33 async function waitForCacheEntry(urlString) { 34 const lci = Services.loadContextInfo.anonymous; 35 const storage = Services.cache2.diskCacheStorage(lci); 36 const uri = Services.io.newURI(urlString); 37 38 await TestUtils.waitForCondition(() => { 39 try { 40 return storage.exists(uri, ""); 41 } catch (e) { 42 return false; 43 } 44 }); 45 46 let entry = await new Promise((resolve, reject) => { 47 storage.asyncOpenURI(uri, "", Ci.nsICacheStorage.OPEN_READONLY, { 48 onCacheEntryCheck: () => Ci.nsICacheEntryOpenCallback.ENTRY_WANTED, 49 onCacheEntryAvailable: (foundEntry, isNew, status) => { 50 if (Components.isSuccessCode(status)) { 51 resolve(foundEntry); 52 } else { 53 reject(new Error(`Cache entry operation failed: ${status}`)); 54 } 55 }, 56 }); 57 }); 58 59 await TestUtils.waitForCondition(() => { 60 try { 61 return entry.dataSize > 0; 62 } catch (e) { 63 return false; 64 } 65 }); 66 } 67 68 add_setup(async function () { 69 // Start HTTP server for cache testing 70 gHttpServer = new HttpServer(); 71 gHttpServer.start(-1); 72 73 // Set OHTTP preferences for testing 74 Services.prefs.setCharPref( 75 "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL", 76 "https://example.com/ohttp-config" 77 ); 78 Services.prefs.setCharPref( 79 "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL", 80 "https://example.com/ohttp-relay" 81 ); 82 83 registerCleanupFunction(async () => { 84 await new Promise(resolve => gHttpServer.stop(resolve)); 85 Services.prefs.clearUserPref( 86 "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL" 87 ); 88 Services.prefs.clearUserPref( 89 "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL" 90 ); 91 }); 92 }); 93 94 /** 95 * Test that OHTTP responses populate the cache and subsequent requests use cache. 96 */ 97 add_task(async function test_ohttp_populates_cache_and_cache_hit() { 98 const sandbox = sinon.createSandbox(); 99 100 try { 101 MockOHTTPService.reset(); 102 103 // Stub ObliviousHTTP.getOHTTPConfig to avoid network requests 104 sandbox 105 .stub(ObliviousHTTP, "getOHTTPConfig") 106 .resolves(new Uint8Array([1, 2, 3, 4])); 107 108 const imageURL = "https://example.com/test-cache-population.jpg"; 109 const testURI = createTestOHTTPResourceURI(imageURL); 110 111 // First request - should go via OHTTP and populate cache 112 MockOHTTPService.shouldUseContentTypeHeader = TEST_CONTENT_TYPE_HEADER; 113 const firstChannel = createTestChannel(testURI); 114 115 let firstRequestData = ""; 116 await new Promise(resolve => { 117 const listener = createDataCollectingListener((data, success) => { 118 Assert.ok(success, "First request should succeed"); 119 firstRequestData = data; 120 resolve(); 121 }); 122 123 firstChannel.asyncOpen(listener); 124 }); 125 126 // Verify OHTTP service was called for first request 127 Assert.ok( 128 MockOHTTPService.channelCreated, 129 "Should call OHTTP service for cache miss" 130 ); 131 Assert.equal( 132 MockOHTTPService.totalChannels, 133 1, 134 "Should make one OHTTP request" 135 ); 136 Assert.greater( 137 firstRequestData.length, 138 0, 139 "Should receive data from OHTTP" 140 ); 141 142 // Reset mock service state for second request 143 MockOHTTPService.channelCreated = false; 144 145 await waitForCacheEntry(imageURL); 146 Assert.ok(true, "Found cache entry."); 147 148 // Second request - should hit cache without calling OHTTP 149 const secondChannel = createTestChannel(testURI); 150 151 let secondRequestData = ""; 152 let cacheHit = false; 153 await new Promise(resolve => { 154 const listener = createDataCollectingListener((data, success) => { 155 Assert.ok(success, "Second request should succeed"); 156 secondRequestData = data; 157 cacheHit = true; 158 resolve(); 159 }); 160 161 secondChannel.asyncOpen(listener); 162 }); 163 164 // Verify cache hit behavior 165 Assert.ok(cacheHit, "Second request should succeed from cache"); 166 Assert.equal( 167 secondChannel.contentType, 168 TEST_CONTENT_TYPE, 169 "Got the right content type from the cache" 170 ); 171 Assert.equal( 172 secondChannel.contentCharset, 173 TEST_CONTENT_CHARSET, 174 "Got the right content charset from the cache" 175 ); 176 Assert.ok( 177 !MockOHTTPService.channelCreated, 178 "Should not call OHTTP service for cache hit" 179 ); 180 Assert.equal( 181 MockOHTTPService.totalChannels, 182 1, 183 "Should still be only one OHTTP request" 184 ); 185 Assert.equal( 186 firstRequestData, 187 secondRequestData, 188 "Cache hit should return same data as original request" 189 ); 190 } finally { 191 sandbox.restore(); 192 } 193 }); 194 195 /** 196 * Test OHTTP fallback when cache miss occurs. 197 */ 198 add_task(async function test_ohttp_fallback_on_cache_miss() { 199 const sandbox = sinon.createSandbox(); 200 201 try { 202 // Reset mock service state 203 MockOHTTPService.reset(); 204 205 // Stub ObliviousHTTP.getOHTTPConfig to avoid network requests 206 sandbox 207 .stub(ObliviousHTTP, "getOHTTPConfig") 208 .resolves(new Uint8Array([1, 2, 3, 4])); 209 210 // Use a URL that won't be in cache 211 const uncachedImageURL = `https://localhost:${gHttpServer.identity.primaryPort}/uncached-image.jpg`; 212 const testURI = createTestOHTTPResourceURI(uncachedImageURL); 213 const channel = createTestChannel(testURI); 214 215 let loadCompleted = false; 216 let receivedData = false; 217 await new Promise(resolve => { 218 const listener = createCompletionListener((success, hasData) => { 219 loadCompleted = success; 220 receivedData = hasData; 221 resolve(); 222 }); 223 224 channel.asyncOpen(listener); 225 }); 226 227 // Verify that the mock OHTTP service was called 228 Assert.ok( 229 MockOHTTPService.channelCreated, 230 "Should call OHTTP service when cache miss occurs" 231 ); 232 Assert.ok(loadCompleted, "Should load successfully via mocked OHTTP"); 233 Assert.ok(receivedData, "Should receive data via mocked OHTTP"); 234 235 // Verify the correct URLs were passed to the OHTTP service 236 Assert.equal( 237 MockOHTTPService.lastTargetURI.spec, 238 uncachedImageURL, 239 "Should request correct target URL via OHTTP" 240 ); 241 Assert.equal( 242 MockOHTTPService.lastRelayURI.spec, 243 "https://example.com/ohttp-relay", 244 "Should use correct relay URL" 245 ); 246 } finally { 247 sandbox.restore(); 248 } 249 });