Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: netwerk/test/unit/xpcshell.toml
/**
* Tests for HTTP Compression Dictionary Brotli (dcb) compression functionality
* - Dictionary-based Brotli compression and decompression
* - Content integrity verification with dcb encoding
* - Available-Dictionary header integration for compression
* - Error handling for missing/invalid dictionaries
* - Compression window size limits and edge cases
*/
"use strict";
// Load cache helpers
const { NodeHTTPSServer } = ChromeUtils.importESModule(
);
// Test dictionaries optimized for compression testing
// Since we're not actually brotli-encoding, all decodes will yield 15 bytes
const DCB_TEST_DICTIONARIES = {
html_common: {
id: "html-dict",
content:
'<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
expected_length: 15,
pattern: "*.html",
type: "raw",
},
html_common_no_dictionary: {
id: "html-dict",
content:
'<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
expected_length: 196,
pattern: "*.html",
type: "raw",
},
api_json: {
id: "api-dict",
content:
'{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
expected_length: 15,
pattern: "/api/*",
type: "raw",
},
api_v1: {
id: "longer-match-dict",
content:
'{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
expected_length: 15,
pattern: "/api/v1/*",
type: "raw",
},
js_common: {
id: "js-dict",
content:
"function(){return this;};var=function();const=function();let=function();",
expected_length: 15,
pattern: "*.js",
type: "raw",
},
large_dict: {
id: "large-dict",
content: "REPEATED_PATTERN_".repeat(1000), // ~1.5MB dictionary
expected_length: 15,
pattern: "/large/*",
type: "raw",
},
};
// Test content designed to compress well with dictionaries
const DCB_TEST_CONTENT = {
html_page:
'<html><head><title>Test Page</title></head><body><div class="container"><p>This is test content that should compress well with the HTML dictionary.</p><p>More content here.</p></div></body></html>',
api_response:
'{"status":"success","data":{"id":12345,"name":"Test User","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-02T12:00:00Z"},"message":"User retrieved successfully","errors":[]}',
api_v1:
'{"status":"success","data":{"id":12345,"name":"Test User","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-02T12:00:00Z"},"message":"User retrieved successfully","errors":[]}',
js_code:
'function testFunction(){return this.value;};var result=function(){console.log("test");};const API_URL=function(){return "https://api.example.com";};let userData=function(){return {id:1,name:"test"};}',
large_content: "REPEATED_PATTERN_DATA_CHUNK_".repeat(50000), // Content that will compress well with large dictionary
jpeg: "ARBITRARY_DATA_".repeat(1000),
};
let server = null;
let requestLog = []; // Track requests for verification
// Create channel for dictionary requests
function makeChan(url) {
let chan = NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
return chan;
}
function channelOpenPromise(chan) {
return new Promise(resolve => {
function finish(req, buffer) {
resolve([req, buffer]);
}
// CL_EXPECT_GZIP is needed if we're transferring compressed data; else it asserts content-length
// equals the data length. (We could also not send content-length)
chan.asyncOpen(
new ChannelListener(
finish,
null,
CL_ALLOW_UNKNOWN_CL | CL_IGNORE_DELAYS | CL_EXPECT_GZIP
)
);
});
}
// Setup DCB test server with dictionaries and compressed content endpoints
async function setupDCBTestServer() {
let httpServer = new NodeHTTPSServer();
await httpServer.start();
// Dictionary endpoints - store dictionaries for later compression use
await httpServer.registerPathHandler(
"/dict/html",
function (request, response) {
const DCB_TEST_DICTIONARIES = {
html_common: {
id: "html-dict",
content:
'<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
pattern: "*.html",
type: "raw",
},
};
let dict = DCB_TEST_DICTIONARIES.html_common;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
await httpServer.registerPathHandler(
"/dict/api",
function (request, response) {
const DCB_TEST_DICTIONARIES = {
api_json: {
id: "api-dict",
content:
'{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
pattern: "/api/*",
type: "raw",
},
};
let dict = DCB_TEST_DICTIONARIES.api_json;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
await httpServer.registerPathHandler(
"/dict/js",
function (request, response) {
const DCB_TEST_DICTIONARIES = {
js_common: {
id: "js-dict",
content:
"function(){return this;};var=function();const=function();let=function();",
pattern: "*.js",
type: "raw",
},
};
let dict = DCB_TEST_DICTIONARIES.js_common;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
await httpServer.registerPathHandler(
"/dict/large",
function (request, response) {
const DCB_TEST_DICTIONARIES = {
large_dict: {
id: "large-dict",
content: "REPEATED_PATTERN_".repeat(1000), // ~1.5MB dictionary
pattern: "/large/*",
type: "raw",
},
};
let dict = DCB_TEST_DICTIONARIES.large_dict;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
// Basic dictionary with valid Use-As-Dictionary header
await httpServer.registerPathHandler(
"/dict/basic",
function (request, response) {
const TEST_DICTIONARIES = {
basic: {
id: "basic-dict",
content: "BASIC_DICTIONARY_DATA",
pattern: "/api/*",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.basic;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
// Dictionary with longer match value
await httpServer.registerPathHandler(
"/dict/longer",
function (request, response) {
const TEST_DICTIONARIES = {
specific: {
id: "longer-match-dict",
content:
'{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
pattern: "/api/v1/*",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.specific;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
"Cache-Control": "max-age=3600",
});
response.end(dict.content, "binary");
}
);
registerCleanupFunction(async () => {
try {
await httpServer.stop();
} catch (e) {
// Ignore server stop errors during cleanup
}
});
return httpServer;
}
async function sync_to_server() {
if (server.processId) {
await server.execute(`global.requestLog = ${JSON.stringify(requestLog)};`);
} else {
dump("Server not running?\n");
}
}
async function sync_from_server() {
if (server.processId) {
dump(`*** requestLog: ${JSON.stringify(requestLog)}\n`);
requestLog = await server.execute(`global.requestLog`);
} else {
dump("Server not running? (from)\n");
}
}
// Calculate expected SHA-256 hash for dictionary content
async function calculateDictionaryHash(content) {
const encoded = new TextEncoder().encode(content);
const digest = await crypto.subtle.digest("SHA-256", encoded);
return btoa(String.fromCharCode(...new Uint8Array(digest))); // base64
}
// Verify dcb decompression result
function verifyDCBResponse(channel, data, dictionary) {
// XXX verify decoded content once we use real Brotli encoding
Assert.equal(data.length, dictionary.expected_length);
try {
// Note: since we remove dcb encoding in the parent process, we can't see
// it in Content-Encoding here
var contentEncoding;
channel.getOriginalResponseHeader("Content-Encoding", {
visitHeader: function visitOrg(aName, aValue) {
contentEncoding = aValue;
},
});
Assert.equal;
if (contentEncoding === "dcb") {
return true;
}
} catch (e) {
// Content-Encoding header not present or not dcb
}
return false;
}
// Setup dcb-aware server endpoint
async function registerDCBEndpoint(
httpServer,
path,
dictionary,
content,
shouldCompress = true
) {
// We have to put all values and functions referenced in the handler into
// this string which will be turned into a function for the handler, because
// NodeHTTPSServer handlers can't access items in the local or global scopes of the
// containing file
let func = `
let path = "${path}";
let dictionary = ${JSON.stringify(dictionary)};
let content = '${content}';
let shouldCompress = ${shouldCompress};
let availableDict = "";
let hasDictHeader = false;
// Get content type based on file path
function getContentTypeForPath(path) {
if (path.endsWith('.html')) return 'text/html; charset=utf-8';
if (path.endsWith('.js')) return 'application/javascript';
if (path.includes('/api/')) return 'application/json';
return 'text/plain; charset=utf-8';
}
// Calculate compression ratio
function calculateCompressionRatio(original, compressed) {
if (typeof original === 'string') original = original.length;
if (typeof compressed === 'string') compressed = compressed.length;
return original / compressed;
}
// Simulate dcb compression (for server responses)
function simulateDCBCompression(content, dictionary) {
// Note: Real implementation would use actual Brotli compression
// For testing, we simulate with compression markers and realistic size reduction
let simulatedCompressedSize = Math.floor(content.length * 0.4); // Simulate 60% savings
// This needs to be something that the brotli decoder will correctly read, even though this
// will produce the wrong output
let compressedData = "\x21\x38\x00\x04COMPRESSED_DATA\x03";
return {
compressedData: "\xff\x44\x43\x42" + "12345678901234567890123456789012" + compressedData,
originalSize: content.length,
compressedSize: compressedData.length + 36,
compressionRatio: calculateCompressionRatio(content.length, simulatedCompressedSize + 36)
};
}
if (request.headers && request.headers['available-dictionary']) {
availableDict = request.headers['available-dictionary'];
hasDictHeader = true;
} else {
shouldCompress = false;
}
// Log the request for analysis
global.requestLog[global.requestLog.length] = {
path: path,
hasAvailableDict: hasDictHeader,
availableDict: availableDict,
method: request.method
};
if (shouldCompress && hasDictHeader && availableDict.includes(dictionary.hash)) {
// Simulate dcb compression
let compressed = simulateDCBCompression(content, dictionary);
response.writeHead(200, {
"Content-Encoding": "dcb",
"Content-Type": getContentTypeForPath(path),
"Content-Length": compressed.compressedSize.toString(),
});
// In a real implementation, this would be actual compressed brotli data
// For testing, we simulate the compressed response
// Note: these aren't real dictionaries; we've prepended a dummy header
// to pass the requirements for a Brotli dictionary - 4 byte magic number
// plus 32 bytes of hash (which we don't currently check, nor does Brotli).
response.end(compressed.compressedData, "binary");
} else {
// Serve uncompressed
response.writeHead(200, {
"Content-Type": getContentTypeForPath(path),
"Content-Length": content.length,
});
response.end(content, "binary");
}
`;
let handler = new Function("request", "response", func);
return httpServer.registerPathHandler(path, handler);
}
// Verify dictionary is stored in cache (reused from previous tests)
function verifyDictionaryStored(url, shouldExist, callback) {
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, callback);
}
async function setupDicts() {
requestLog = [];
await sync_to_server();
// Store all test dictionaries and calculate their hashes
const dictPaths = [
"/dict/html",
"/dict/api",
"/dict/js",
"/dict/large",
"/dict/longer",
];
const dictKeys = [
"html_common",
"api_json",
"js_common",
"large_dict",
"api_v1",
];
for (let i = 0; i < dictPaths.length; i++) {
let path = dictPaths[i];
let dictKey = dictKeys[i];
let url = `${server.origin()}${path}`;
dump(
`registering dictionary ${path} for match patter ${DCB_TEST_DICTIONARIES[dictKey].patterh}\n`
);
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
// Calculate and store hash for later use
DCB_TEST_DICTIONARIES[dictKey].hash =
":" +
(await calculateDictionaryHash(DCB_TEST_DICTIONARIES[dictKey].content)) +
":";
// Verify dictionary content matches
Assert.equal(
data,
DCB_TEST_DICTIONARIES[dictKey].content,
`Dictionary content matches`
);
// Verify dictionary was stored
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
}
dump(`**** DCB test setup complete. Dictionaries stored with hashes.\n`);
}
add_setup(async function () {
if (!server) {
server = await setupDCBTestServer();
}
// Setup baseline dictionaries for compression testing
// Clear any existing cache
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
evict_cache_entries("all", lci);
await setupDicts();
});
// Test basic dictionary-compressed Brotli functionality
add_task(async function test_basic_dcb_compression() {
dump("**** test_basic_dcb_compression\n");
requestLog = [];
await sync_to_server();
// Setup DCB endpoint for HTML content
let dict = DCB_TEST_DICTIONARIES.html_common;
let content = DCB_TEST_CONTENT.html_page;
await registerDCBEndpoint(server, "/test.html", dict, content, true);
let url = `${server.origin()}/test.html`;
let chan = makeChan(url);
let [request, data] = await channelOpenPromise(chan);
// Check if DCB compression was used
let usedDCB = verifyDCBResponse(
request.QueryInterface(Ci.nsIHttpChannel),
data,
dict
);
Assert.ok(usedDCB, "DCB compression should be used");
});
// Test correct dictionary selection for dcb compression
add_task(async function test_dcb_dictionary_selection() {
requestLog = [];
await sync_to_server();
dump("**** Testing DCB dictionary selection\n");
// Test specific pattern matching for dictionary selection
let htmlDict = DCB_TEST_DICTIONARIES.html_common;
let apiDict = DCB_TEST_DICTIONARIES.api_json;
// Register endpoints that should match different dictionaries
await registerDCBEndpoint(
server,
"/specific-test.html",
htmlDict,
DCB_TEST_CONTENT.html_page,
true
);
await registerDCBEndpoint(
server,
"/api/specific-test",
apiDict,
DCB_TEST_CONTENT.api_response,
true
);
// Test HTML dictionary selection
let htmlUrl = `${server.origin()}/specific-test.html`;
let htmlChan = makeChan(htmlUrl);
let [, htmlData] = await channelOpenPromise(htmlChan);
Assert.greater(
htmlData.length,
0,
"HTML dictionary selection test should have content"
);
// Check if correct dictionary was used
await sync_from_server();
let htmlLogEntry = requestLog.find(
entry => entry.path === "/specific-test.html"
);
Assert.ok(
htmlLogEntry && htmlLogEntry.hasAvailableDict,
"Dictionary selection test: HTML endpoint received Available-Dictionary header"
);
// Test API dictionary selection
let apiUrl = `${server.origin()}/api/specific-test`;
let apiChan = makeChan(apiUrl);
let [, apiData] = await channelOpenPromise(apiChan);
Assert.greater(
apiData.length,
0,
"API dictionary selection test should have content"
);
// Check if correct dictionary was used
await sync_from_server();
let apiLogEntry = requestLog.find(
entry => entry.path === "/api/specific-test"
);
Assert.ok(
apiLogEntry && apiLogEntry.hasAvailableDict,
"Dictionary selection test: API endpoint received Available-Dictionary header"
);
});
// Test behavior when dictionary is missing/unavailable
add_task(async function test_dcb_missing_dictionary() {
requestLog = [];
await sync_to_server();
dump("**** Testing DCB missing dictionary\n");
// Create a fake dictionary that won't be found
let fakeDict = {
id: "missing-dict",
hash: "fake_hash_that_does_not_exist",
content: "This dictionary was not stored",
expected_length: DCB_TEST_CONTENT.jpeg.length,
};
// *.jpeg Doesn't match any of the patterns in DCB_TEST_DICTIONARIES
await registerDCBEndpoint(
server,
"/missing-dict-test.jpeg",
fakeDict,
DCB_TEST_CONTENT.jpeg,
false
);
let url = `${server.origin()}/missing-dict-test.jpeg`;
let chan = makeChan(url);
let [request, data] = await channelOpenPromise(chan);
// Should get uncompressed content when dictionary is missing
Assert.greater(
data.length,
0,
"Missing dictionary test should still return content"
);
// Verify no dcb compression was applied
let usedDCB = verifyDCBResponse(
request.QueryInterface(Ci.nsIHttpChannel),
data,
fakeDict
);
Assert.ok(!usedDCB, "We should not get DCB encoding for a fake item");
});
// Test IETF spec compliance for dcb encoding
add_task(async function test_dcb_header_compliance() {
requestLog = [];
await sync_to_server();
dump("**** Testing DCB header compliance\n");
let dict = DCB_TEST_DICTIONARIES.api_json;
await registerDCBEndpoint(
server,
"/api/compliance-test",
dict,
DCB_TEST_CONTENT.api_response,
true
);
let url = `${server.origin()}/api/compliance-test`;
let chan = makeChan(url);
let [request, data] = await channelOpenPromise(chan);
Assert.greater(data.length, 0, "IETF compliance test should have content");
let httpChannel = request.QueryInterface(Ci.nsIHttpChannel);
// Verify proper Content-Type preservation
try {
let contentType = httpChannel.getResponseHeader("Content-Type");
Assert.ok(
contentType.includes("application/json"),
"Content-Type should be preserved through compression"
);
} catch (e) {
Assert.ok(false, "Content-Type header should be present");
}
// Check for proper dcb handling
await sync_from_server();
let logEntry = requestLog.find(
entry => entry.path === "/api/compliance-test"
);
Assert.ok(
logEntry && logEntry.hasAvailableDict,
"Must have available-dictionary in header compliance"
);
// Verify Available-Dictionary follows IETF Structured Field Byte-Sequence format
// According to RFC 8941, byte sequences are enclosed in colons: :base64data:
let availableDict = logEntry.availableDict;
Assert.ok(
availableDict.startsWith(":"),
"Available-Dictionary should start with ':' (IETF Structured Field Byte-Sequence format)"
);
Assert.ok(
availableDict.endsWith(":"),
"Available-Dictionary should end with ':' (IETF Structured Field Byte-Sequence format)"
);
Assert.greater(
availableDict.length,
2,
"Available-Dictionary should contain base64 data between colons"
);
// Extract the base64 content between the colons
let base64Content = availableDict.slice(1, -1);
Assert.greater(
base64Content.length,
0,
"Available-Dictionary should have base64 content"
);
// Basic validation that it looks like base64 (contains valid base64 characters)
let base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
Assert.ok(
base64Regex.test(base64Content),
"Available-Dictionary content should be valid base64"
);
dump(`**** IETF compliance test: Available-Dictionary = ${availableDict}\n`);
});
// Test that DCB compression stops working after dictionary cache eviction
add_task(async function test_dcb_compression_after_cache_eviction() {
requestLog = [];
await sync_to_server();
dump("**** Testing DCB compression after cache eviction\n");
// Use a specific dictionary for this test
let dict = DCB_TEST_DICTIONARIES.html_common;
let dict2 = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
let testContent = DCB_TEST_CONTENT.html_page;
let testPath = "/cache-eviction-test.html";
let dictUrl = `${server.origin()}/dict/html`;
let contentUrl = `${server.origin()}${testPath}`;
// Step 1: Ensure dictionary is in cache by fetching it
dump("**** Step 1: Loading dictionary into cache\n");
let dictChan = makeChan(dictUrl);
let [, dictData] = await channelOpenPromise(dictChan);
Assert.equal(dictData, dict.content, "Dictionary loaded successfully");
// Verify dictionary is cached
await new Promise(resolve => {
verifyDictionaryStored(dictUrl, true, () => {
resolve();
});
});
// Step 2: Set up DCB endpoint and test compression works
dump("**** Step 2: Testing DCB compression with cached dictionary\n");
await registerDCBEndpoint(server, testPath, dict, testContent, true);
// Clear request log before testing
requestLog = [];
await sync_to_server();
let chan1 = makeChan(contentUrl);
let [req1, data1] = await channelOpenPromise(chan1);
Assert.greater(data1.length, 0, "Should receive content before eviction");
// Check if DCB compression was used (should be true with cached dictionary)
let usedDCB1 = verifyDCBResponse(
req1.QueryInterface(Ci.nsIHttpChannel),
data1,
dict
);
Assert.ok(usedDCB1, "DCB compression should be used");
// Step 3: Evict the dictionary from cache
dump("**** Step 3: Evicting dictionary from cache\n");
// Evict the dictionary cache entry
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
evict_cache_entries("all", lci);
// Force cache sync to ensure everything is written
await new Promise(resolve => {
syncWithCacheIOThread(resolve, true);
});
dump("**** Step 3.5: verify no longer cache\n");
// Verify dictionary is no longer cached
await new Promise(resolve => {
verifyDictionaryStored(dictUrl, false, () => {
resolve();
});
});
// Step 4: Test that compression no longer works after eviction
dump("**** Step 4: Testing DCB compression after dictionary eviction\n");
let chan2 = makeChan(contentUrl);
let [req2, data2] = await channelOpenPromise(chan2);
Assert.greater(
data2.length,
0,
"Should still receive content after eviction"
);
// Check if DCB compression was used (should be false without cached dictionary)
Assert.ok(
!verifyDCBResponse(req2.QueryInterface(Ci.nsIHttpChannel), data2, dict2),
"DCB compression should not be used without dictionary"
);
// XXX We can only check this if we actually brotli-compress the data
// Content should still be delivered in both cases, just not compressed in the second case
//Assert.equal(data1.length, data2.length,
// "Content length should be the same whether compressed or not (in our test simulation)");
dump("**** Cache eviction test completed successfully\n");
});
// Test HTTP redirect (302) with dictionary-compressed content
add_task(async function test_dcb_with_http_redirect() {
await setupDicts();
dump("**** Testing HTTP redirect (302) with dictionary-compressed content\n");
let dict = DCB_TEST_DICTIONARIES.html_common;
let content = DCB_TEST_CONTENT.html_page;
await registerDCBEndpoint(server, "/test.html", dict, content, true);
let originalPath = "/redirect/original";
let finalPath = "/test.html";
let originalUrl = `${server.origin()}${originalPath}`;
let finalUrl = `${server.origin()}${finalPath}`;
// Step 1: Set up redirect handler that returns 302 to final URL
let redirectFunc = `
let finalPath = "${finalPath}";
// Log the request for analysis
global.requestLog[global.requestLog.length] = {
path: "${originalPath}",
method: request.method,
redirectTo: finalPath,
hasAvailableDict: !!request.headers['available-dictionary'],
availableDict: request.headers['available-dictionary'] || null
};
response.writeHead(302, {
"Location": finalPath,
"Cache-Control": "no-cache"
});
response.end("Redirecting...");
`;
let redirectHandler = new Function("request", "response", redirectFunc);
await server.registerPathHandler(originalPath, redirectHandler);
// Step 2: Set up final endpoint with DCB compression capability
await registerDCBEndpoint(server, finalPath, dict, content, true);
// Clear request log before testing
requestLog = [];
await sync_to_server();
// Step 3: Request the original URL that redirects to potentially DCB-compressed content
let chan = makeChan(originalUrl);
let [req, data] = await channelOpenPromise(chan);
// Step 4: Verify redirect worked correctly
let finalUri = req.QueryInterface(Ci.nsIHttpChannel).URI.spec;
Assert.equal(
finalUri,
finalUrl,
"Final URI should match the redirected URL after 302 redirect"
);
// Verify we received some content
Assert.greater(data.length, 0, "Should receive content after redirect");
// Step 5: Check request log to verify both requests were logged
await sync_from_server();
// Should have two entries: redirect request and final request
let redirectEntry = requestLog.find(entry => entry.path === originalPath);
let finalEntry = requestLog.find(entry => entry.path === finalPath);
Assert.ok(redirectEntry, "Redirect request should be logged");
Assert.ok(finalEntry, "Final request should be logged");
// Step 6: Verify Available-Dictionary header handling
// Note: The redirect request may or may not have Available-Dictionary header depending on implementation
// The important thing is that the final request has it
if (redirectEntry.hasAvailableDict) {
dump(`**** Redirect request includes Available-Dictionary header\n`);
} else {
dump(
`**** Redirect request does not include Available-Dictionary header (expected)\n`
);
}
// Note: With redirects, Available-Dictionary headers may not be preserved
Assert.ok(
finalEntry.hasAvailableDict,
"Final request includes Available-Dictionary header"
);
// Available-Dictionary header should contain the dictionary hash for final request
Assert.ok(
finalEntry.availableDict.includes(dict.hash),
"Final request Available-Dictionary should contain correct dictionary hash"
);
// Step 7: Check if DCB compression was applied
Assert.ok(
verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
"DCB compression successfully applied after redirect"
);
});
// Test invalid Use-As-Dictionary headers - missing match parameter
add_task(async function test_use_as_dictionary_invalid_missing_match() {
// Invalid dictionary headers
await server.registerPathHandler(
"/dict/invalid-missing-match",
function (request, response) {
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `id="missing-match-dict", type=raw`,
"Cache-Control": "max-age=3600",
});
response.end("INVALID_MISSING_MATCH_DATA", "binary");
}
);
let url = `${server.origin()}/dict/invalid-missing-match`;
let chan = makeChan(url);
let [req, data] = await channelOpenPromise(chan);
// Verify dictionary content matches
Assert.equal(
data,
"INVALID_MISSING_MATCH_DATA",
"Set up missing match dictionary"
);
let dict = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
let content = DCB_TEST_CONTENT.html_page;
await registerDCBEndpoint(
server,
"/invalid/missing-match",
dict,
content,
true
);
chan = makeChan(url);
[req, data] = await channelOpenPromise(chan);
Assert.equal(data, content, "Content received");
// Verify invalid header was not processed as dictionary
Assert.ok(
!verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
"DCB compression should not be used when dictionary has no match="
);
// Invalid dictionary should not be processed as dictionary
dump("**** Missing match parameter test complete\n");
});
// Test invalid Use-As-Dictionary headers - empty id parameter
add_task(async function test_use_as_dictionary_invalid_empty_id() {
await server.registerPathHandler(
"/dict/invalid-empty-id",
function (request, response) {
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="/invalid/*", id="", type=raw`,
"Cache-Control": "max-age=3600",
});
response.end("INVALID_EMPTY_ID_DATA", "binary");
}
);
let url = `${server.origin()}/dict/invalid-empty-id`;
let chan = makeChan(url);
let [req, data] = await channelOpenPromise(chan);
// Verify dictionary content matches
Assert.equal(data, "INVALID_EMPTY_ID_DATA", "Set up empty id dictionary");
let dict = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
let content = DCB_TEST_CONTENT.html_page;
await registerDCBEndpoint(server, "/invalid/empty-id", dict, content, true);
chan = makeChan(url);
[req, data] = await channelOpenPromise(chan);
Assert.equal(data, content, "non-compressed content received");
Assert.ok(
!verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
"DCB compression should not be used with dictionary with empty id"
);
dump("**** Empty id parameter test complete\n");
});
// Test Available-Dictionary request header generation
add_task(async function test_available_dictionary_header_generation() {
requestLog = [];
await sync_to_server();
// Calculate expected hash for basic dictionary
let expectedHashB64 = await calculateDictionaryHash(
DCB_TEST_DICTIONARIES.api_json.content
);
// Setup DCB endpoint for HTML content
let dict = DCB_TEST_DICTIONARIES.html_common;
let content = DCB_TEST_CONTENT.api_response;
await registerDCBEndpoint(server, "/api/test", dict, content, true);
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
Assert.equal(data, DCB_TEST_CONTENT.api_response, "Resource content matches");
// Check request log to see if Available-Dictionary header was sent
await sync_from_server();
let logEntry = requestLog.find(entry => entry.path === "/api/test");
Assert.ok(
logEntry && logEntry.availableDict != null,
"Available-Dictionary header should be present"
);
if (logEntry && logEntry.availableDict != null) {
// Verify IETF Structured Field Byte-Sequence format
Assert.ok(
logEntry.availableDict.startsWith(":"),
"Available-Dictionary should start with ':' (IETF Structured Field format)"
);
Assert.ok(
logEntry.availableDict.endsWith(":"),
"Available-Dictionary should end with ':' (IETF Structured Field format)"
);
// Verify base64 content
let base64Content = logEntry.availableDict.slice(1, -1);
let base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
Assert.ok(
base64Regex.test(base64Content),
"Available-Dictionary content should be valid base64"
);
Assert.equal(
logEntry.availableDict,
":" + expectedHashB64 + ":",
"Available-Dictionary has the right hash"
);
}
dump("**** Available-Dictionary generation test complete\n");
});
// Test Available-Dictionary header for specific pattern matching
add_task(async function test_available_dictionary_specific_patterns() {
requestLog = [];
await sync_to_server();
let dict = DCB_TEST_DICTIONARIES.api_v1;
let content = DCB_TEST_CONTENT.api_v1;
await registerDCBEndpoint(server, "/api/v1/test", dict, content, true);
let chan = makeChan(url);
await channelOpenPromise(chan);
// Check for Available-Dictionary header
await sync_from_server();
let logEntry = requestLog.find(entry => entry.path === "/api/v1/test");
Assert.ok(
logEntry && logEntry.availableDict != null,
"Available-Dictionary header should be present for /api/v1/*"
);
if (logEntry && logEntry.availableDict != null) {
// Should match both /api/v1/* (longer-dict) and /api/* (basic-dict) patterns
// It should always use the longer match, which would be /api/v1/*
Assert.equal(
logEntry.availableDict,
DCB_TEST_DICTIONARIES.api_v1.hash,
"Longer match pattern for a dictionary should be used"
);
}
dump("**** Specific pattern matching test complete\n");
});
// Test Available-Dictionary header absence for no matching patterns
add_task(async function test_available_dictionary_no_match() {
requestLog = [];
await sync_to_server();
let dict = DCB_TEST_DICTIONARIES.html_common;
let content = "NO MATCH TEST DATA";
await registerDCBEndpoint(server, "/nomatch/test", dict, content, true);
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
Assert.equal(data, "NO MATCH TEST DATA", "No match content received");
// Check that no Available-Dictionary header was sent
await sync_from_server();
let logEntry = requestLog.find(entry => entry.path === "/nomatch/test");
Assert.ok(logEntry, "Request should be logged");
if (logEntry) {
Assert.equal(
logEntry.availableDict,
"",
"Available-Dictionary should be null for no match"
);
}
dump("**** No match test complete\n");
});
// Cleanup
add_task(async function cleanup() {
// Clear cache
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
evict_cache_entries("all", lci);
dump("**** DCB compression tests completed.\n");
});