invidious/spec/http_server/handlers/static_assets_handler_spec.cr
syeopite c012206f80
Improve cache size check to be more performant
Summing the sizes of each cached file every time is very inefficient.
Instead we can simply store the cache size in an constant and increase
it everytime a file is added into the cache.
2025-06-03 17:10:10 -07:00

234 lines
8.8 KiB
Crystal

# Due to the way that specs are handled this file cannot be run together with
# everything else without causing a compile time error that'll be incredibly
# annoying to resolve.
#
# TODO: Create different spec categories that can then be ran through make.
# An implementation of this can be seen with the tests for the Crystal compiler itself.
#
# For now run this with `crystal spec spec/http_server/handlers/static_assets_handler_spec.cr -Drunning_by_self`
{% skip_file if compare_versions(Crystal::VERSION, "1.17.0-dev") < 0 || !flag?(:running_by_self) %}
require "http"
require "spectator"
require "../../../src/invidious/http_server/static_assets_handler.cr"
private def get_static_assets_handler
return Invidious::HttpServer::StaticAssetsHandler.new "spec/http_server/handlers/static_assets_handler", directory_listing: false
end
# Slightly modified version of `handle` function from
#
# https://github.com/crystal-lang/crystal/blob/3f369d2c721e9462d9f6126cb0bcd4c6992f0225/spec/std/http/server/handlers/static_file_handler_spec.cr#L5
private def handle(request, handler : HTTP::Handler? = nil, decompress : Bool = false)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
if !handler
handler = get_static_assets_handler
get_static_assets_handler.call context
else
handler.call(context)
end
response.close
io.rewind
HTTP::Client::Response.from_io(io, decompress: decompress)
end
# Makes and yields a temporary file with the given prefix
private def make_temporary_file(prefix, contents = nil, &)
tempfile = File.tempfile(prefix, "static_assets_handler_spec", dir: "spec/http_server/handlers/static_assets_handler")
file_link = "/#{File.basename(tempfile.path)}"
yield tempfile, file_link
ensure
tempfile.try &.delete
end
# Changes the contents of the temporary file after yield
private def cycle_temporary_file_contents(temporary_file, initial, &)
temporary_file.rewind << initial
temporary_file.rewind.flush
yield
temporary_file.rewind << "something else"
temporary_file.rewind.flush
end
# Get relative file path to a file within the static_assets_handler folder
macro get_file_path(basename)
"spec/http_server/handlers/static_assets_handler/#{ {{basename}} }"
end
Spectator.describe StaticAssetsHandler do
it "Can serve a file" do
response = handle HTTP::Request.new("GET", "/test.txt")
expect(response.status_code).to eq(200)
expect(response.body).to eq(File.read(get_file_path("test.txt")))
end
it "Can serve cached file" do
make_temporary_file("cache_test") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "foo") do
expect(temporary_file.rewind.gets_to_end).to eq("foo")
# Should get cached by the first run
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("foo")
end
# Temporary file is updated after `cycle_temporary_file_contents` is called
# but if the file is successfully cached then we'll only get the original
# contents.
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("foo")
end
end
it "Adds cache headers" do
response = handle HTTP::Request.new("GET", "/test.txt")
expect(response.headers["cache_control"]).to eq("max-age=2629800")
end
context "Can handle range requests" do
it "Can serve range request" do
headers = HTTP::Headers{"Range" => "bytes=0-2"}
response = handle HTTP::Request.new("GET", "/test.txt", headers)
expect(response.status_code).to eq(206)
expect(response.headers["Content-Range"]?).to eq "bytes 0-2/11"
expect(response.body).to eq "Hel"
end
it "Will cache entire file even if doing partial requests" do
make_temporary_file("range_cache") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world") do
handle HTTP::Request.new("GET", file_link, HTTP::Headers{"Range" => "bytes=0-2"})
end
# Second request shouldn't have changed
headers = HTTP::Headers{"Range" => "bytes=3-8"}
response = handle HTTP::Request.new("GET", file_link, headers)
expect(response.status_code).to eq(206)
expect(response.body).to eq "lo wor"
end
end
end
context "Is able to support compression" do
def decompressed(string : String)
decompressed = Compress::Gzip::Reader.open(IO::Memory.new(string)) do |gzip|
gzip.gets_to_end
end
return expect(decompressed)
end
it "For full file requests" do
handler = HTTP::CompressHandler.new
handler.next = get_static_assets_handler()
make_temporary_file("check decompression handler") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world") do
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
decompressed(response.body).to eq("Hello world")
end
# Are cached requests working?
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
decompressed(response.body).to eq("Hello world")
# Able to retrieve non gzipped file?
response = handle HTTP::Request.new("GET", file_link), handler: handler
expect(response.body).to eq("Hello world")
expect(response.headers).to_not have_key("Content-Encoding")
end
end
# Inspired by the equivalent tests from upstream
it "For partial file requests" do
handler = HTTP::CompressHandler.new
handler.next = get_static_assets_handler()
make_temporary_file("check_decompression_handler_on_partial_requests") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world this is a very long string") do
range_response_results = {
"10-20/38" => "d this is a",
"0-0/38" => "H",
"5-9/38" => " worl",
}
range_request_header_value = {"10-20", "5-9", "0-0"}.join(',')
range_response_header_value = range_response_results.keys
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Range" => "bytes=#{range_request_header_value}", "Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
# Decompress response
response = HTTP::Client::Response.new(
status: response.status,
headers: response.headers,
body_io: Compress::Gzip::Reader.new(IO::Memory.new(response.body)),
)
count = 0
MIME::Multipart.parse(response) do |headers, part|
part_range = headers["Content-Range"][6..]
expect(part_range).to be_within(range_response_header_value)
expect(part.gets_to_end).to eq(range_response_results[part_range])
count += 1
end
expect(count).to eq(3)
end
# Is the file cached?
temporary_file << "Something else"
temporary_file.flush.rewind
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
decompressed(response.body).to eq("Hello world this is a very long string")
end
end
end
it "Will not cache additional files if the cache limit is reached" do
5.times do |times|
data = "a" * 1_000_000
make_temporary_file("test cache size limit #{times}") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, data) do
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq(data)
end
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq(data)
end
end
# Cache should be 5 mb so no more files will be cached.
make_temporary_file("test cache size limit uncached") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "a") do
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("a")
end
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to_not eq("a")
end
end
after_each { Invidious::HttpServer::StaticAssetsHandler.clear_cache }
end