# 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