Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: netwerk/test/unit/xpcshell.toml
/**
* Tests for HTTP Compression Dictionary storage functionality
* - Use-As-Dictionary header parsing and validation
* - Dictionary storage in cache with proper metadata
* - Pattern matching and hash validation
* - Error handling and edge cases
*/
"use strict";
// Load cache helpers
const { NodeHTTPSServer } = ChromeUtils.importESModule(
);
// Test data constants
const TEST_DICTIONARIES = {
small: {
id: "test-dict-small",
content: "COMMON_PREFIX_DATA_FOR_COMPRESSION",
pattern: "/api/v1/*",
type: "raw",
},
large: {
id: "test-dict-large",
content: "A".repeat(1024 * 100), // 100KB dictionary
pattern: "*.html",
type: "raw",
},
large_url: {
id: "test-dict-large-url",
content: "large URL content",
pattern: "large",
type: "raw",
},
too_large_url: {
id: "test-dict-too-large-url",
content: "too large URL content",
pattern: "too_large",
type: "raw",
},
};
let server = null;
add_setup(async function () {
if (!server) {
server = await setupServer();
}
// Setup baseline dictionaries for compression testing
// Clear any existing cache
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
evict_cache_entries("all", lci);
});
// Utility function to calculate SHA-256 hash
async function calculateSHA256(data) {
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(Ci.nsICryptoHash.SHA256);
// Convert string to UTF-8 bytes
let bytes = new TextEncoder().encode(data);
hasher.update(bytes, bytes.length);
return hasher.finish(false);
}
// Setup dictionary test server
async function setupServer() {
let httpServer = new NodeHTTPSServer();
await httpServer.start();
// Basic dictionary endpoint
await httpServer.registerPathHandler(
"/dict/small",
function (request, response) {
// Test data constants
const TEST_DICTIONARIES = {
small: {
id: "test-dict-small",
content: "COMMON_PREFIX_DATA_FOR_COMPRESSION",
pattern: "/api/v1/*",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.small;
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 expiration
await httpServer.registerPathHandler(
"/dict/expires",
function (request, response) {
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": `match="expires/*", id="expires-dict", type=raw`,
"Cache-Control": "max-age=1",
});
response.end("EXPIRING_DICTIONARY_DATA", "binary");
}
);
// Dictionary with invalid header
await httpServer.registerPathHandler(
"/dict/invalid",
function (request, response) {
global.test = 1;
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": "invalid-header-format",
});
response.end("INVALID_DICTIONARY_DATA", "binary");
}
);
// Large dictionary
await httpServer.registerPathHandler(
"/dict/large",
function (request, response) {
// Test data constants
const TEST_DICTIONARIES = {
large: {
id: "test-dict-large",
content: "A".repeat(1024 * 100), // 100KB dictionary
pattern: "*.html",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.large;
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");
}
);
// Large dictionary URL
await httpServer.registerPathHandler(
"/dict/large/" + "A".repeat(1024 * 20),
function (request, response) {
// Test data constants
const TEST_DICTIONARIES = {
large_url: {
id: "test-dict-large-url",
content: "large URL content",
pattern: "large",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.large_url;
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");
}
);
// Too Large dictionary URL
await httpServer.registerPathHandler(
"/dict/large/" + "B".repeat(1024 * 100),
function (request, response) {
// Test data constants
const TEST_DICTIONARIES = {
too_large_url: {
id: "test-dict-too-large-url",
content: "too large URL content",
pattern: "too_large",
type: "raw",
},
};
let dict = TEST_DICTIONARIES.too_large_url;
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;
}
// Verify dictionary is stored in cache
function verifyDictionaryStored(url, shouldExist, callback) {
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, callback);
}
// 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]);
}
chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL));
});
}
// Test basic dictionary storage with Use-As-Dictionary header
add_task(async function test_basic_dictionary_storage() {
// Clear any existing cache
evict_cache_entries("all");
let dict = TEST_DICTIONARIES.small;
let chan = makeChan(url);
let [req, data] = await channelOpenPromise(chan);
Assert.equal(data, dict.content, "Dictionary content matches");
// Verify Use-As-Dictionary header was processed
try {
let headerValue = req.getResponseHeader("Use-As-Dictionary");
Assert.ok(
headerValue.includes(`id="${dict.id}"`),
"Header contains correct ID"
);
Assert.ok(
headerValue.includes(`match="${dict.pattern}"`),
"Header contains correct pattern"
);
} catch (e) {
Assert.ok(false, "Use-As-Dictionary header should be present");
}
// Check that dictionary is stored in cache
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
});
// Test Use-As-Dictionary header parsing with various formats
add_task(async function test_dictionary_header_parsing() {
const headerTests = [
{
header: 'match="*", id="dict1", type=raw',
valid: true,
description: "Basic valid header",
},
{
header: 'match="/api/*", id="api-dict", type=raw',
valid: true,
description: "Path pattern header",
},
{
header: 'match="*.js", id="js-dict"',
valid: true,
description: "Header without type (should default to raw)",
},
{
header: 'id="dict1", type=raw',
valid: false,
description: "Missing match parameter",
},
{
header: 'match="*"',
valid: false,
description: "Missing id parameter",
},
{
header: 'match="*", id="", type=raw',
valid: false,
description: "Empty id parameter",
},
];
let testIndex = 0;
for (let test of headerTests) {
let testPath = `/dict/header-test-${testIndex++}`;
let func = `
global.testIndex = 0;
let test = ${JSON.stringify(test)};
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": test.header,
});
// We won't be using this, so it doesn't really matter
response.end("HEADER_TEST_DICT_" + global.testIndex++, "binary");
`;
let handler = new Function("request", "response", func);
await server.registerPathHandler(testPath, handler);
let chan = makeChan(url);
await channelOpenPromise(chan);
// XXX test if we have a dictionary entry. Need new APIs to let me test it,
// or we can read dict:<origin> and look for this entry
// Note: Invalid dictionary headers still create regular cache entries,
// they just aren't processed as dictionaries. So all should exist in cache.
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
}
});
// Test dictionary hash calculation and validation
add_task(async function test_dictionary_hash_calculation() {
dump("**** testing hashes\n");
let dict = TEST_DICTIONARIES.small;
// Calculate expected hash
let expectedHash = await calculateSHA256(dict.content);
Assert.greater(expectedHash.length, 0, "Hash should be calculated");
let chan = makeChan(url);
await channelOpenPromise(chan);
// Calculate expected hash
let hashCalculatedHash = await calculateSHA256(dict.content);
Assert.greater(hashCalculatedHash.length, 0, "Hash should be calculated");
// Check cache entry exists
await new Promise(resolve => {
let lci = Services.loadContextInfo.custom(false, {
partitionKey: `(https,localhost)`,
});
asyncOpenCacheEntry(
url,
"disk",
Ci.nsICacheStorage.OPEN_READONLY,
lci,
function (status, entry) {
Assert.equal(status, Cr.NS_OK, "Cache entry should exist");
Assert.ok(entry, "Entry should not be null");
// Check if entry has dictionary metadata
try {
let metaData = entry.getMetaDataElement("use-as-dictionary");
Assert.ok(metaData, "Dictionary metadata should exist");
// Verify metadata contains hash information
// Note: The exact format may vary based on implementation
Assert.ok(
metaData.includes(dict.id),
"Metadata should contain dictionary ID"
);
} catch (e) {
// Dictionary metadata might be stored differently
dump(`Dictionary metadata access failed: ${e}\n`);
}
resolve();
}
);
});
});
// Test dictionary expiration handling
add_task(async function test_dictionary_expiration() {
dump("**** testing expiration\n");
// Fetch dictionary with 1-second expiration
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
Assert.equal(data, "EXPIRING_DICTIONARY_DATA", "Dictionary content matches");
// Note: Testing actual expiration behavior requires waiting and is complex
// For now, just verify the dictionary was fetched
// XXX FIX!
});
// Test multiple dictionaries per origin with different patterns
add_task(async function test_multiple_dictionaries_per_origin() {
dump("**** test multiple dictionaries per origin\n");
// Register multiple dictionary endpoints for same origin
await server.registerPathHandler("/dict/api", function (request, response) {
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": 'match="/api/*", id="api-dict", type=raw',
});
response.end("API_DICTIONARY_DATA", "binary");
});
await server.registerPathHandler("/dict/web", function (request, response) {
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Use-As-Dictionary": 'match="/web/*", id="web-dict", type=raw',
});
response.end("WEB_DICTIONARY_DATA", "binary");
});
// Fetch both dictionaries
let apiChan = makeChan(apiUrl);
let [, apiData] = await channelOpenPromise(apiChan);
Assert.equal(
apiData,
"API_DICTIONARY_DATA",
"API dictionary content matches"
);
let webChan = makeChan(webUrl);
let [, webData] = await channelOpenPromise(webChan);
Assert.equal(
webData,
"WEB_DICTIONARY_DATA",
"Web dictionary content matches"
);
// Verify both dictionaries are stored
await new Promise(resolve => {
verifyDictionaryStored(apiUrl, true, () => {
verifyDictionaryStored(webUrl, true, resolve);
});
});
});
// Test dictionary size limits and validation
add_task(async function test_dictionary_size_limits() {
dump("**** test size limits\n");
let dict = TEST_DICTIONARIES.large;
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
Assert.equal(data, dict.content, "Large dictionary content matches");
Assert.equal(data.length, dict.content.length, "Dictionary size correct");
// Verify large dictionary is stored
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
});
// Test error handling with invalid dictionary headers
add_task(async function test_invalid_dictionary_headers() {
dump("**** test error handling\n");
let chan = makeChan(url);
let [, data] = await channelOpenPromise(chan);
Assert.equal(
data,
"INVALID_DICTIONARY_DATA",
"Invalid dictionary content received"
);
// Invalid dictionary should not be stored as dictionary
// but the regular cache entry should exist
await new Promise(resolve => {
asyncOpenCacheEntry(
url,
"disk",
Ci.nsICacheStorage.OPEN_READONLY,
null,
function (status, entry) {
if (status === Cr.NS_OK && entry) {
// Regular cache entry should exist
// Note: Don't call entry.close() as it doesn't exist on this interface
}
// But it should not be processed as a dictionary
resolve();
}
);
});
});
// Test cache integration and persistence
add_task(async function test_dictionary_cache_persistence() {
dump("**** test persistence\n");
// Force cache sync to ensure everything is written
await new Promise(resolve => {
syncWithCacheIOThread(resolve, true);
});
// Get cache statistics before
await new Promise(resolve => {
get_device_entry_count("disk", null, entryCount => {
Assert.greater(entryCount, 0, "Cache should have entries");
resolve();
});
});
// Verify our test dictionaries are still present
let chan = makeChan(smallUrl);
await channelOpenPromise(chan);
await new Promise(resolve => {
verifyDictionaryStored(smallUrl, true, resolve);
});
});
// Test very long url which should fit in metadata
add_task(async function test_long_dictionary_url() {
// Clear any existing cache
evict_cache_entries("all");
let url =
let dict = TEST_DICTIONARIES.large_url;
let chan = makeChan(url);
let [req, data] = await channelOpenPromise(chan);
Assert.equal(data, dict.content, "Dictionary content matches");
// Check that dictionary is stored in cache
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
// Verify Use-As-Dictionary header was processed and it's an active dictionary
chan = makeChan(url);
[req, data] = await channelOpenPromise(chan);
try {
let headerValue = req.getRequestHeader("Available-Dictionary");
Assert.ok(headerValue.includes(`:`), "Header contains a hash");
} catch (e) {
Assert.ok(
false,
"Available-Dictionary header should be present with long URL for dictionary"
);
}
});
// Test url too long to store in metadata
add_task(async function test_too_long_dictionary_url() {
// Clear any existing cache
evict_cache_entries("all");
let url =
let dict = TEST_DICTIONARIES.too_large_url;
let chan = makeChan(url);
let [req, data] = await channelOpenPromise(chan);
Assert.equal(data, dict.content, "Dictionary content matches");
// Check that dictionary is stored in cache (even if it's not a dictionary)
await new Promise(resolve => {
verifyDictionaryStored(url, true, resolve);
});
// Verify Use-As-Dictionary header was NOT processed and active due to 64K limit to metadata
// Since we can't store it on disk, we can't offer it as a dictionary. If we change the
// metadata limit, this will need to change
chan = makeChan(url);
[req, data] = await channelOpenPromise(chan);
try {
// we're just looking to see if it throws
// eslint-disable-next-line no-unused-vars
let headerValue = req.getRequestHeader("Available-Dictionary");
Assert.ok(false, "Too-long dictionary was offered in Available-Dictionary");
} catch (e) {
Assert.ok(
true,
"Available-Dictionary header should not be present with a too-long URL for dictionary"
);
}
});
// Cleanup
add_task(async function cleanup() {
// Clear cache
evict_cache_entries("all");
dump("**** all done\n");
});