2026-05-03 14:09:18 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <AK/ByteBuffer.h>
|
2026-05-15 09:58:56 -03:00
|
|
|
#include <LibCore/ImmutableBytes.h>
|
2026-05-03 14:09:18 -03:00
|
|
|
#include <LibHTTP/Cache/CacheRequest.h>
|
|
|
|
|
#include <LibHTTP/Cache/DiskCache.h>
|
|
|
|
|
#include <LibHTTP/Cache/Utilities.h>
|
|
|
|
|
#include <LibHTTP/HeaderList.h>
|
|
|
|
|
#include <LibTest/TestCase.h>
|
|
|
|
|
#include <LibURL/Parser.h>
|
|
|
|
|
|
|
|
|
|
struct TestCacheRequest final : public HTTP::CacheRequest {
|
|
|
|
|
virtual bool is_revalidation_request() const override { return false; }
|
|
|
|
|
virtual void notify_request_unblocked(Badge<HTTP::DiskCache>) override { }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static URL::URL parse_url(StringView url)
|
|
|
|
|
{
|
|
|
|
|
return URL::Parser::basic_parse(url).release_value();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static NonnullRefPtr<HTTP::HeaderList> create_cacheable_request_headers()
|
|
|
|
|
{
|
|
|
|
|
return HTTP::HeaderList::create({
|
|
|
|
|
{ HTTP::TEST_CACHE_ENABLED_HEADER, "1"sv },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static NonnullRefPtr<HTTP::HeaderList> create_cacheable_response_headers()
|
|
|
|
|
{
|
|
|
|
|
return HTTP::HeaderList::create({
|
|
|
|
|
{ "Cache-Control"sv, "max-age=60"sv },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static HTTP::CacheEntryWriter& create_cache_entry(HTTP::DiskCache& disk_cache, TestCacheRequest& request, URL::URL const& url, HTTP::HeaderList const& request_headers)
|
|
|
|
|
{
|
|
|
|
|
Optional<HTTP::CacheEntryWriter&> writer;
|
|
|
|
|
|
|
|
|
|
disk_cache.create_entry(request, url, "GET"sv, request_headers, UnixDateTime::now())
|
|
|
|
|
.visit(
|
|
|
|
|
[&](Optional<HTTP::CacheEntryWriter&> cache_entry_writer) {
|
|
|
|
|
writer = cache_entry_writer;
|
|
|
|
|
},
|
|
|
|
|
[](HTTP::DiskCache::CacheHasOpenEntry) {
|
|
|
|
|
FAIL("Cache entry was unexpectedly open");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
VERIFY(writer.has_value());
|
|
|
|
|
return *writer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(associated_data_round_trips_with_cache_entry)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = create_cacheable_response_headers();
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('hello');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(writer.flush(request_headers, response_headers));
|
|
|
|
|
|
|
|
|
|
auto bytecode = TRY_OR_FAIL(ByteBuffer::copy("bytecode"sv.bytes()));
|
2026-05-03 15:04:35 -03:00
|
|
|
EXPECT(TRY_OR_FAIL(disk_cache.store_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, bytecode.bytes())));
|
2026-05-03 14:09:18 -03:00
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
auto retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
2026-05-03 14:09:18 -03:00
|
|
|
VERIFY(retrieved_bytecode.has_value());
|
|
|
|
|
EXPECT_EQ(retrieved_bytecode->bytes(), bytecode.bytes());
|
|
|
|
|
|
2026-05-15 11:34:54 -03:00
|
|
|
auto retrieved_bytecode_file = TRY_OR_FAIL(disk_cache.retrieve_associated_data_file(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
|
|
|
|
VERIFY(retrieved_bytecode_file.has_value());
|
|
|
|
|
auto mapped_bytecode = TRY_OR_FAIL(Core::ImmutableBytes::map_from_fd_range_and_close(retrieved_bytecode_file->fd, "bytecode"sv, retrieved_bytecode_file->offset, retrieved_bytecode_file->size));
|
|
|
|
|
EXPECT_EQ(mapped_bytecode.bytes(), bytecode.bytes());
|
|
|
|
|
|
2026-05-03 14:09:18 -03:00
|
|
|
disk_cache.remove_entries_accessed_since(UnixDateTime::earliest());
|
|
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
2026-05-03 14:09:18 -03:00
|
|
|
EXPECT(!retrieved_bytecode.has_value());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(replacing_cache_entry_removes_associated_data)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = create_cacheable_response_headers();
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('old');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(writer.flush(request_headers, response_headers));
|
|
|
|
|
|
|
|
|
|
auto bytecode = TRY_OR_FAIL(ByteBuffer::copy("bytecode"sv.bytes()));
|
2026-05-03 15:04:35 -03:00
|
|
|
EXPECT(TRY_OR_FAIL(disk_cache.store_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, bytecode.bytes())));
|
2026-05-03 14:09:18 -03:00
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
auto retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
2026-05-03 14:09:18 -03:00
|
|
|
VERIFY(retrieved_bytecode.has_value());
|
|
|
|
|
|
|
|
|
|
auto replacement_request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto replacement_response_headers = create_cacheable_response_headers();
|
|
|
|
|
auto& replacement_writer = create_cache_entry(disk_cache, request, url, *replacement_request_headers);
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.write_status_and_reason(200, "OK"_string, *replacement_request_headers, *replacement_response_headers));
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.write_data("console.log('new');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.flush(replacement_request_headers, replacement_response_headers));
|
|
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
2026-05-03 14:09:18 -03:00
|
|
|
EXPECT(!retrieved_bytecode.has_value());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 09:58:56 -03:00
|
|
|
TEST_CASE(flush_returns_mappable_body_file)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = create_cacheable_response_headers();
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('hello');"sv.bytes()));
|
|
|
|
|
|
|
|
|
|
auto body_file = TRY_OR_FAIL(writer.flush_and_take_body_file(request_headers, response_headers));
|
|
|
|
|
auto body = TRY_OR_FAIL(Core::ImmutableBytes::map_from_fd_range_and_close(body_file.fd, "cache body"sv, body_file.offset, body_file.size));
|
|
|
|
|
EXPECT_EQ(body.bytes(), "console.log('hello');"sv.bytes());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 13:40:14 -03:00
|
|
|
TEST_CASE(replacing_cache_entry_keeps_existing_body_mapping_stable)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = create_cacheable_response_headers();
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('old');"sv.bytes()));
|
|
|
|
|
|
|
|
|
|
auto old_body_file = TRY_OR_FAIL(writer.flush_and_take_body_file(request_headers, response_headers));
|
|
|
|
|
auto old_body = TRY_OR_FAIL(Core::ImmutableBytes::map_from_fd_range_and_close(old_body_file.fd, "old cache body"sv, old_body_file.offset, old_body_file.size));
|
|
|
|
|
EXPECT_EQ(old_body.bytes(), "console.log('old');"sv.bytes());
|
|
|
|
|
|
|
|
|
|
auto replacement_request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto replacement_response_headers = create_cacheable_response_headers();
|
|
|
|
|
auto& replacement_writer = create_cache_entry(disk_cache, request, url, *replacement_request_headers);
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.write_status_and_reason(200, "OK"_string, *replacement_request_headers, *replacement_response_headers));
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.write_data("console.log('new');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(replacement_writer.flush(replacement_request_headers, replacement_response_headers));
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(old_body.bytes(), "console.log('old');"sv.bytes());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
TEST_CASE(associated_data_round_trips_with_explicit_vary_key)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = HTTP::HeaderList::create({
|
|
|
|
|
{ HTTP::TEST_CACHE_ENABLED_HEADER, "1"sv },
|
|
|
|
|
{ "Origin"sv, "https://origin.example"sv },
|
|
|
|
|
});
|
|
|
|
|
auto mismatched_request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = HTTP::HeaderList::create({
|
|
|
|
|
{ "Cache-Control"sv, "max-age=60"sv },
|
|
|
|
|
{ "Vary"sv, "Origin"sv },
|
|
|
|
|
});
|
|
|
|
|
auto vary_key = HTTP::create_vary_key(*request_headers, *response_headers);
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('hello');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(writer.flush(request_headers, response_headers));
|
|
|
|
|
|
|
|
|
|
auto bytecode = TRY_OR_FAIL(ByteBuffer::copy("bytecode"sv.bytes()));
|
|
|
|
|
EXPECT(!TRY_OR_FAIL(disk_cache.store_associated_data(url, "GET"sv, *mismatched_request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, bytecode.bytes())));
|
|
|
|
|
EXPECT(TRY_OR_FAIL(disk_cache.store_associated_data(url, "GET"sv, *mismatched_request_headers, vary_key, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, bytecode.bytes())));
|
|
|
|
|
|
|
|
|
|
auto retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *mismatched_request_headers, vary_key, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
|
|
|
|
VERIFY(retrieved_bytecode.has_value());
|
|
|
|
|
EXPECT_EQ(retrieved_bytecode->bytes(), bytecode.bytes());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 14:09:18 -03:00
|
|
|
TEST_CASE(associated_data_participates_in_cache_eviction)
|
|
|
|
|
{
|
|
|
|
|
auto disk_cache = MUST(HTTP::DiskCache::create(HTTP::DiskCache::Mode::Testing));
|
|
|
|
|
TestCacheRequest request;
|
|
|
|
|
|
|
|
|
|
auto url = parse_url("https://example.com/script.js"sv);
|
|
|
|
|
auto request_headers = create_cacheable_request_headers();
|
|
|
|
|
auto response_headers = create_cacheable_response_headers();
|
|
|
|
|
|
|
|
|
|
auto& writer = create_cache_entry(disk_cache, request, url, *request_headers);
|
|
|
|
|
TRY_OR_FAIL(writer.write_status_and_reason(200, "OK"_string, *request_headers, *response_headers));
|
|
|
|
|
TRY_OR_FAIL(writer.write_data("console.log('hello');"sv.bytes()));
|
|
|
|
|
TRY_OR_FAIL(writer.flush(request_headers, response_headers));
|
|
|
|
|
|
|
|
|
|
disk_cache.set_maximum_disk_cache_size(80);
|
|
|
|
|
auto bytecode = TRY_OR_FAIL(ByteBuffer::create_zeroed(100));
|
2026-05-03 15:04:35 -03:00
|
|
|
EXPECT(!TRY_OR_FAIL(disk_cache.store_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, bytecode.bytes())));
|
2026-05-03 14:09:18 -03:00
|
|
|
|
2026-05-03 15:04:35 -03:00
|
|
|
auto retrieved_bytecode = TRY_OR_FAIL(disk_cache.retrieve_associated_data(url, "GET"sv, *request_headers, {}, HTTP::CacheEntryAssociatedData::JavaScriptBytecode));
|
2026-05-03 14:09:18 -03:00
|
|
|
EXPECT(!retrieved_bytecode.has_value());
|
|
|
|
|
}
|