mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-07 21:59:54 +00:00
LibWeb: Add some tests that exercise the HTTP disk cache
Our HTTP disk cache is currently manually tested against various sites. This patch adds some tests to cover various scenarios, including non- cacheable responses, expired responses, and revalidation. In order to ensure we hit the disk cache in RequestServer, we must disable the in-memory cache in WebContent.
This commit is contained in:
parent
a4c8e39b99
commit
813986237e
Notes:
github-actions[bot]
2025-11-20 08:35:21 +00:00
Author: https://github.com/trflynn89
Commit: 813986237e
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6861
Reviewed-by: https://github.com/gmta ✅
7 changed files with 353 additions and 0 deletions
|
|
@ -2848,6 +2848,11 @@ void set_http_cache_enabled(bool const enabled)
|
|||
g_http_cache_enabled = enabled;
|
||||
}
|
||||
|
||||
bool http_cache_enabled()
|
||||
{
|
||||
return g_http_cache_enabled;
|
||||
}
|
||||
|
||||
void clear_http_cache()
|
||||
{
|
||||
HTTPCache::the().clear_cache();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ void set_sec_fetch_user_header(Infrastructure::Request&);
|
|||
void append_fetch_metadata_headers_for_request(Infrastructure::Request&);
|
||||
|
||||
WEB_API void set_http_cache_enabled(bool enabled);
|
||||
WEB_API bool http_cache_enabled();
|
||||
WEB_API void clear_http_cache();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <LibWeb/DOM/EventTarget.h>
|
||||
#include <LibWeb/DOM/NodeList.h>
|
||||
#include <LibWeb/DOMURL/DOMURL.h>
|
||||
#include <LibWeb/Fetch/Fetching/Fetching.h>
|
||||
#include <LibWeb/HTML/HTMLElement.h>
|
||||
#include <LibWeb/HTML/Navigable.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
|
@ -298,6 +299,13 @@ void Internals::expire_cookies_with_time_offset(WebIDL::LongLong seconds)
|
|||
page().client().page_did_expire_cookies_with_time_offset(AK::Duration::from_seconds(seconds));
|
||||
}
|
||||
|
||||
bool Internals::set_http_memory_cache_enabled(bool enabled)
|
||||
{
|
||||
auto was_enabled = Web::Fetch::Fetching::http_cache_enabled();
|
||||
Web::Fetch::Fetching::set_http_cache_enabled(enabled);
|
||||
return was_enabled;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static
|
||||
String Internals::get_computed_role(DOM::Element& element)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ public:
|
|||
void enable_cookies_on_file_domains();
|
||||
void expire_cookies_with_time_offset(WebIDL::LongLong seconds);
|
||||
|
||||
bool set_http_memory_cache_enabled(bool enabled);
|
||||
|
||||
String get_computed_role(DOM::Element& element);
|
||||
String get_computed_label(DOM::Element& element);
|
||||
String get_computed_aria_level(DOM::Element& element);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ interface Internals {
|
|||
undefined enableCookiesOnFileDomains();
|
||||
undefined expireCookiesWithTimeOffset(long long seconds);
|
||||
|
||||
boolean setHttpMemoryCacheEnabled(boolean enabled);
|
||||
|
||||
DOMString getComputedRole(Element element);
|
||||
DOMString getComputedLabel(Element element);
|
||||
DOMString getComputedAriaLevel(Element element);
|
||||
|
|
|
|||
1
Tests/LibWeb/Text/expected/Cache/http-disk-cache.txt
Normal file
1
Tests/LibWeb/Text/expected/Cache/http-disk-cache.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
PASS!
|
||||
334
Tests/LibWeb/Text/input/Cache/http-disk-cache.html
Normal file
334
Tests/LibWeb/Text/input/Cache/http-disk-cache.html
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
const TEST_CACHE_ENABLED_HEADER = "X-Ladybird-Enable-Disk-Cache";
|
||||
const TEST_CACHE_STATUS_HEADER = "X-Ladybird-Disk-Cache-Status";
|
||||
const TEST_CACHE_REQUEST_TIME_OFFSET = "X-Ladybird-Request-Time-Offset";
|
||||
const TEST_CACHE_RESPOND_WITH_NOT_MODIFIED = "X-Ladybird-Respond-With-Not-Modified";
|
||||
|
||||
const server = httpTestServer();
|
||||
|
||||
let anyTestFailed = false;
|
||||
let lastTestPath = null;
|
||||
|
||||
async function createRequest(path, options) {
|
||||
lastTestPath = path;
|
||||
|
||||
if (typeof options === "undefined") {
|
||||
options = {};
|
||||
}
|
||||
if (!options.method) {
|
||||
options.method = "GET";
|
||||
}
|
||||
if (!options.status) {
|
||||
options.status = 200;
|
||||
}
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
|
||||
await server.createEcho("OPTIONS", path, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Access-Control-Allow-Headers": `${TEST_CACHE_ENABLED_HEADER}, ${TEST_CACHE_REQUEST_TIME_OFFSET}, ${TEST_CACHE_RESPOND_WITH_NOT_MODIFIED}`,
|
||||
"Access-Control-Allow-Methods": options.method,
|
||||
"Access-Control-Allow-Origin": location.origin,
|
||||
},
|
||||
});
|
||||
|
||||
options.headers["Access-Control-Allow-Origin"] = location.origin;
|
||||
options.headers["Access-Control-Expose-Headers"] = TEST_CACHE_STATUS_HEADER;
|
||||
|
||||
return server.createEcho(options.method, path, {
|
||||
status: options.status,
|
||||
headers: options.headers,
|
||||
reflect_headers_in_body: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function cacheFetch(url, options) {
|
||||
if (typeof options === "undefined") {
|
||||
options = {};
|
||||
}
|
||||
if (!options.method) {
|
||||
options.method = "GET";
|
||||
}
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
|
||||
options.headers[TEST_CACHE_ENABLED_HEADER] = "1";
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
mode: "cors",
|
||||
});
|
||||
}
|
||||
|
||||
function expectCacheStatus(url, response, status) {
|
||||
const result = response.headers.get(TEST_CACHE_STATUS_HEADER);
|
||||
|
||||
if (result !== status) {
|
||||
println(`Expected ${url} to contain a cache status of '${status}': received: '${result}'`);
|
||||
anyTestFailed = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
let url, response;
|
||||
|
||||
// Non-GET/HEAD requests are not cached.
|
||||
await (async () => {
|
||||
for (const method of ["POST", "PUT", "DELETE"]) {
|
||||
url = await createRequest(`/cache-test/${method.toLowerCase()}`, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Cache-Control": "max-age=999",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url, { method });
|
||||
expectCacheStatus(url, response, "not-cached");
|
||||
}
|
||||
})();
|
||||
|
||||
// Responses without a Cache-Control or Expires header are not cached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/missing-headers");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "not-cached");
|
||||
})();
|
||||
|
||||
// Responses with a no-store directive are not cached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/no-store", {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "not-cached");
|
||||
})();
|
||||
|
||||
// Responses with only a max-age directive are cached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/max-age", {
|
||||
headers: {
|
||||
"Cache-Control": "max-age=5",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
})();
|
||||
|
||||
// Responses with an age less than their max-age directive are cached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/max-age-fresh", {
|
||||
headers: {
|
||||
Age: "2",
|
||||
"Cache-Control": "max-age=5",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
})();
|
||||
|
||||
// Responses with an age equal to their max-age directive are not cached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/max-age-expired", {
|
||||
headers: {
|
||||
Age: "5",
|
||||
"Cache-Control": "max-age=5",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "not-cached");
|
||||
})();
|
||||
|
||||
// Expired responses are cached until their max-age directive is reached.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/expired-and-refreshed", {
|
||||
headers: {
|
||||
Age: "2",
|
||||
"Cache-Control": "max-age=5",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
})();
|
||||
|
||||
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
||||
// without a revalidation attribute results in the cache being refreshed.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/expired-and-refreshed-due-to-missing-revalidation-attributes", {
|
||||
headers: {
|
||||
Age: "2",
|
||||
"Cache-Control": "max-age=5,must-revalidate",
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
})();
|
||||
|
||||
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
||||
// with an invalid revalidation attribute results in the cache being refreshed.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/expired-and-refreshed-due-to-invalid-revalidation-attributes", {
|
||||
headers: {
|
||||
Age: "2",
|
||||
"Cache-Control": "max-age=5,must-revalidate",
|
||||
"Last-Modified": new Date().toUTCString(),
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
// By not attaching a TEST_CACHE_RESPOND_WITH_NOT_MODIFIED header, we tell http-test-server to respond to
|
||||
// revalidation requests with an HTTP 200 (i.e. revalidation fails).
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
})();
|
||||
|
||||
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
||||
// with a valid revalidation attribute results in the cache being revalidated.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/expired-and-revalidated", {
|
||||
headers: {
|
||||
Age: "2",
|
||||
"Cache-Control": "max-age=5,must-revalidate",
|
||||
"Last-Modified": new Date().toUTCString(),
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
||||
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
})();
|
||||
|
||||
// Responses with a no-cache directive must always be revalidated.
|
||||
await (async () => {
|
||||
url = await createRequest("/cache-test/no-cache", {
|
||||
headers: {
|
||||
"Cache-Control": "no-cache",
|
||||
"Last-Modified": new Date().toUTCString(),
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
// By not attaching a TEST_CACHE_RESPOND_WITH_NOT_MODIFIED header, we tell http-test-server to respond to
|
||||
// revalidation requests with an HTTP 200 (i.e. revalidation fails).
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
})();
|
||||
|
||||
// Responses without a Cache-Control header may be heuristically cached based on the Last-Modified header.
|
||||
await (async () => {
|
||||
// Our current heuristic is 10% of the time since the Last-Modified header.
|
||||
url = await createRequest("/cache-test/cache-heuristic", {
|
||||
headers: {
|
||||
"Last-Modified": new Date(Date.now() - 10 * 60 * 60 * 1000).toUTCString(),
|
||||
},
|
||||
});
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
|
||||
response = await cacheFetch(url);
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: 30 * 60,
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "read-from-cache");
|
||||
|
||||
response = await cacheFetch(url, {
|
||||
headers: {
|
||||
[TEST_CACHE_REQUEST_TIME_OFFSET]: 90 * 60,
|
||||
},
|
||||
});
|
||||
expectCacheStatus(url, response, "written-to-cache");
|
||||
})();
|
||||
}
|
||||
|
||||
asyncTest(async done => {
|
||||
// Disable memory cache to ensure all requests reach RequestServer.
|
||||
const httpMemoryCacheWasEnabled = internals.setHttpMemoryCacheEnabled(false);
|
||||
|
||||
runTests()
|
||||
.then(() => {
|
||||
if (!anyTestFailed) {
|
||||
println("PASS!");
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
println(`Caught exception: ${lastTestPath}: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
internals.setHttpMemoryCacheEnabled(httpMemoryCacheWasEnabled);
|
||||
done();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue