invidious/spec/invidious/subtitle_cache_spec.cr

236 lines
8.9 KiB
Crystal

require "../spec_helper"
require "../../src/invidious/user/preferences"
require "../../src/invidious/jobs/base_job"
require "../../src/invidious/jobs/*"
require "../../src/invidious/jobs"
require "../../src/invidious/config"
{% unless @top_level.has_constant?(:CONFIG) %}
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
{% end %}
CONFIG.invidious_companion_key = "1234567890123456"
private def subtitle_response(status_code : Int32, content_type : String, body : String)
headers = HTTP::Headers.new
headers["Content-Type"] = content_type
headers["Access-Control-Allow-Origin"] = "*"
Invidious::SubtitleCache::Response.new(status_code, headers, body)
end
Spectator.describe Invidious::SubtitleCache do
describe "check verification" do
it "verifies valid tokens" do
token = invidious_companion_encrypt("test_video_123")
expect(invidious_companion_verify_check(token, "test_video_123")).to be_true
end
it "rejects tokens with mismatched video id" do
token = invidious_companion_encrypt("test_video_123")
expect(invidious_companion_verify_check(token, "other_video_456")).to be_false
end
it "rejects expired tokens older than 6 hours" do
old_ts = Time.utc.to_unix - (7 * 3600)
encrypted = encrypt_ecb_without_salt("#{old_ts}|test_video_123", CONFIG.invidious_companion_key)
token = Base64.urlsafe_encode(encrypted)
expect(invidious_companion_verify_check(token, "test_video_123")).to be_false
end
it "rejects malformed or empty tokens" do
expect(invidious_companion_verify_check("", "test_video_123")).to be_false
expect(invidious_companion_verify_check("invalid-base64-!@#$", "test_video_123")).to be_false
expect(invidious_companion_verify_check("AAAA", "test_video_123")).to be_false
end
end
describe "SubtitleCache operations" do
it "correctly identifies valid WebVTT" do
expect(Invidious::SubtitleCache.valid_vtt?("WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHello")).to be_true
expect(Invidious::SubtitleCache.valid_vtt?("\uFEFFWEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHello")).to be_true
expect(Invidious::SubtitleCache.valid_vtt?(" \nWEBVTT\n\n")).to be_true
expect(Invidious::SubtitleCache.valid_vtt?("WEBVTT")).to be_true
expect(Invidious::SubtitleCache.valid_vtt?("")).to be_false
expect(Invidious::SubtitleCache.valid_vtt?("WEBVTT-error")).to be_false
expect(Invidious::SubtitleCache.valid_vtt?("{\"error\": \"not found\"}")).to be_false
expect(Invidious::SubtitleCache.valid_vtt?("<!DOCTYPE html><html></html>")).to be_false
end
it "stores and retrieves entries" do
cache = Invidious::SubtitleCache.new
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nTest cue\n"
entry = cache.put("video:123|label:en", vtt, "text/vtt; charset=utf-8")
expect(entry).not_to be_nil
expect(cache.size).to eq(1)
retrieved = cache.get("video:123|label:en")
expect(retrieved).not_to be_nil
expect(retrieved.try &.body).to eq(vtt)
end
it "isolates different languages and labels" do
cache = Invidious::SubtitleCache.new
vtt_en = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nEnglish\n"
vtt_zh = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nChinese\n"
cache.put("video:123|label:English|lang:en|tlang:", vtt_en, "text/vtt")
cache.put("video:123|label:Chinese|lang:zh|tlang:", vtt_zh, "text/vtt")
expect(cache.get("video:123|label:English|lang:en|tlang:").try &.body).to eq(vtt_en)
expect(cache.get("video:123|label:Chinese|lang:zh|tlang:").try &.body).to eq(vtt_zh)
end
it "builds cache keys that isolate delimiter-containing values" do
key_a = Invidious::SubtitleCache.caption_cache_key("video", "English|lang:x", "en", "")
key_b = Invidious::SubtitleCache.caption_cache_key("video", "English", "lang:x", "")
expect(key_a).not_to eq(key_b)
end
it "returns miss on first fetch and hit on subsequent fetch" do
cache = Invidious::SubtitleCache.new
fetch_count = 0
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHello"
result1 = cache.get_or_fetch("video:abc") do
fetch_count += 1
subtitle_response(200, "text/vtt", vtt)
end
expect(result1.cache_status).to eq("miss")
expect(result1.entry.try &.body).to eq(vtt)
expect(fetch_count).to eq(1)
result2 = cache.get_or_fetch("video:abc") do
fetch_count += 1
subtitle_response(200, "text/vtt", vtt)
end
expect(result2.cache_status).to eq("hit")
expect(result2.entry.try &.body).to eq(vtt)
expect(fetch_count).to eq(1)
end
it "coalesces concurrent fetches for the same key" do
cache = Invidious::SubtitleCache.new
fetch_count = 0
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nCoalesced"
done_ch = ::Channel(String).new(5)
5.times do
spawn do
result = cache.get_or_fetch("video:concurrent") do
fetch_count += 1
sleep 0.05.seconds
subtitle_response(200, "text/vtt", vtt)
end
done_ch.send(result.cache_status)
end
end
results = Array(String).new
5.times { results << done_ch.receive }
expect(fetch_count).to eq(1)
expect(results.count("miss")).to eq(1)
expect(results.count("hit")).to eq(4)
end
it "evicts oldest entries when max_entries is exceeded (LRU)" do
cache = Invidious::SubtitleCache.new(max_entries: 2)
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nLine"
cache.put("k1", vtt, "text/vtt")
cache.put("k2", vtt, "text/vtt")
expect(cache.size).to eq(2)
# Access k1 to make it most recently used
cache.get("k1")
# Adding k3 should evict k2 (the oldest)
cache.put("k3", vtt, "text/vtt")
expect(cache.size).to eq(2)
expect(cache.get("k1")).not_to be_nil
expect(cache.get("k3")).not_to be_nil
expect(cache.get("k2")).to be_nil
end
it "rejects entries when entry capacity is zero" do
cache = Invidious::SubtitleCache.new(max_entries: 0)
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nZero capacity"
expect(cache.put("k", vtt, "text/vtt")).to be_nil
expect(cache.size).to eq(0)
byte_limited = Invidious::SubtitleCache.new(max_bytes: 0)
expect(byte_limited.put("k", vtt, "text/vtt")).to be_nil
expect(byte_limited.size).to eq(0)
end
it "matches exact caption proxy paths only" do
regex = Invidious::SubtitleCache::CAPTION_PATH_REGEX
expect(regex.match("/api/v1/captions/video_id").try &.[1]).to eq("video_id")
expect(regex.match("/companion/api/v1/captions/video_id").try &.[1]).to eq("video_id")
expect(regex.match("/api/v1/captions/video_id/extra")).to be_nil
end
it "evicts oldest entries when max_bytes is exceeded" do
cache = Invidious::SubtitleCache.new(max_entries: 10, max_bytes: 60)
vtt1 = "WEBVTT\n\n12345678901234567890" # ~30 bytes
vtt2 = "WEBVTT\n\nABCDEFGHIJABCDEFGHIJ" # ~30 bytes
vtt3 = "WEBVTT\n\nXYZXYZXYZXYZXYZXYZ" # ~27 bytes
cache.put("k1", vtt1, "text/vtt")
cache.put("k2", vtt2, "text/vtt")
expect(cache.size).to eq(2)
# Adding k3 pushes total bytes over 60, evicts k1
cache.put("k3", vtt3, "text/vtt")
expect(cache.get("k1")).to be_nil
expect(cache.get("k2")).not_to be_nil
expect(cache.get("k3")).not_to be_nil
end
it "does not cache failed or non-vtt responses" do
cache = Invidious::SubtitleCache.new
result = cache.get_or_fetch("video:bad") do
subtitle_response(404, "application/json", "{\"error\":\"not found\"}")
end
expect(result.cache_status).to eq("bypass")
expect(result.entry).to be_nil
expect(result.response.try &.body).to eq("{\"error\":\"not found\"}")
expect(result.response.try &.headers["Access-Control-Allow-Origin"]).to eq("*")
expect(cache.size).to eq(0)
end
it "does not cache oversized fetch responses" do
cache = Invidious::SubtitleCache.new
oversized = "WEBVTT\n" + ("x" * (Invidious::SubtitleCache::MAX_ENTRY_BYTES + 1))
result = cache.get_or_fetch("video:oversized") do
subtitle_response(200, "text/vtt", oversized)
end
expect(result.cache_status).to eq("bypass")
expect(result.entry).to be_nil
expect(result.response.try &.body).to eq(oversized)
expect(cache.size).to eq(0)
end
it "stops reading bodies beyond the entry size limit" do
valid_body = "WEBVTT\n"
valid_result = Invidious::SubtitleCache.read_limited_body(IO::Memory.new(valid_body))
expect(valid_result.oversized).to be_false
expect(valid_result.body).to eq(valid_body)
oversized_body = "x" * (Invidious::SubtitleCache::MAX_ENTRY_BYTES + 1)
oversized_result = Invidious::SubtitleCache.read_limited_body(IO::Memory.new(oversized_body))
expect(oversized_result.oversized).to be_true
expect(oversized_result.body).to be_nil
end
end
end