summaryrefslogtreecommitdiffstats
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/helpers/vtt/builder_spec.cr87
-rw-r--r--spec/helpers_spec.cr141
-rw-r--r--spec/i18next_plurals_spec.cr231
-rw-r--r--spec/invidious/hashtag_spec.cr109
-rw-r--r--spec/invidious/helpers_spec.cr56
-rw-r--r--spec/invidious/search/iv_filters_spec.cr370
-rw-r--r--spec/invidious/search/query_spec.cr242
-rw-r--r--spec/invidious/search/yt_filters_spec.cr143
-rw-r--r--spec/invidious/user/imports_spec.cr51
-rw-r--r--spec/invidious/utils_spec.cr46
-rw-r--r--spec/invidious/videos/regular_videos_extract_spec.cr168
-rw-r--r--spec/invidious/videos/scheduled_live_extract_spec.cr111
-rw-r--r--spec/locales_spec.cr29
-rw-r--r--spec/parsers_helper.cr35
-rw-r--r--spec/spec_helper.cr18
15 files changed, 1667 insertions, 170 deletions
diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr
new file mode 100644
index 00000000..dc1f4613
--- /dev/null
+++ b/spec/helpers/vtt/builder_spec.cr
@@ -0,0 +1,87 @@
+require "../../spec_helper.cr"
+
+MockLines = ["Line 1", "Line 2"]
+MockLinesWithEscapableCharacter = ["<Line 1>", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"]
+
+Spectator.describe "WebVTT::Builder" do
+ it "correctly builds a vtt file" do
+ result = WebVTT.build do |vtt|
+ 2.times do |i|
+ vtt.cue(
+ Time::Span.new(seconds: i),
+ Time::Span.new(seconds: i + 1),
+ MockLines[i]
+ )
+ end
+ end
+
+ expect(result).to eq([
+ "WEBVTT",
+ "",
+ "00:00:00.000 --> 00:00:01.000",
+ "Line 1",
+ "",
+ "00:00:01.000 --> 00:00:02.000",
+ "Line 2",
+ "",
+ "",
+ ].join('\n'))
+ end
+
+ it "correctly builds a vtt file with setting fields" do
+ setting_fields = {
+ "Kind" => "captions",
+ "Language" => "en",
+ }
+
+ result = WebVTT.build(setting_fields) do |vtt|
+ 2.times do |i|
+ vtt.cue(
+ Time::Span.new(seconds: i),
+ Time::Span.new(seconds: i + 1),
+ MockLines[i]
+ )
+ end
+ end
+
+ expect(result).to eq([
+ "WEBVTT",
+ "Kind: captions",
+ "Language: en",
+ "",
+ "00:00:00.000 --> 00:00:01.000",
+ "Line 1",
+ "",
+ "00:00:01.000 --> 00:00:02.000",
+ "Line 2",
+ "",
+ "",
+ ].join('\n'))
+ end
+
+ it "properly escapes characters" do
+ result = WebVTT.build do |vtt|
+ 4.times do |i|
+ vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i])
+ end
+ end
+
+ expect(result).to eq([
+ "WEBVTT",
+ "",
+ "00:00:00.000 --> 00:00:01.000",
+ "&lt;Line 1&gt;",
+ "",
+ "00:00:01.000 --> 00:00:02.000",
+ "&amp;Line 2&gt;",
+ "",
+ "00:00:02.000 --> 00:00:03.000",
+ "&lrm;Line&rlm; 3",
+ "",
+ "00:00:03.000 --> 00:00:04.000",
+ "&nbsp;Line 4",
+ "",
+ "",
+ ].join('\n'))
+ end
+end
diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr
deleted file mode 100644
index ed3a3d48..00000000
--- a/spec/helpers_spec.cr
+++ /dev/null
@@ -1,141 +0,0 @@
-require "kemal"
-require "openssl/hmac"
-require "pg"
-require "protodec/utils"
-require "spec"
-require "yaml"
-require "../src/invidious/helpers/*"
-require "../src/invidious/channels"
-require "../src/invidious/comments"
-require "../src/invidious/playlists"
-require "../src/invidious/search"
-require "../src/invidious/trending"
-require "../src/invidious/users"
-
-CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
-
-describe "Helper" do
- describe "#produce_channel_videos_url" do
- it "correctly produces url for requesting page `x` of a channel's videos" do
- produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw").should eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
-
- produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
-
- produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20).should eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
-
- produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
- end
- end
-
- describe "#produce_channel_search_continuation" do
- it "correctly produces token for searching a specific channel" do
- produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
-
- produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
- end
- end
-
- describe "#produce_channel_playlists_url" do
- it "correctly produces a /browse_ajax URL with the given UCID and cursor" do
- produce_channel_playlists_url("UCCj956IF62FbT7Gouszaj9w", "AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW").should eq("/browse_ajax?continuation=4qmFsgLNARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrABRWdsd2JHRjViR2x6ZEhNd0FqZ0JZQUZxQUxnQkFIcG1VVlZzVUdFeGF6VlNWa1ozWVZZNWJtVlhOSGhZTVVaNVVtNVdZVTFZU214VWFtZDRXREF4VG1KVmEzaFhWekZ6VVcxS2MyUjZhSEZPTUhCSlUxaFNSbEpyWXpGaFJHUjRXVEJ3VlZSdFVUQldlbXcwVGxaR01XRXhPVVJXYkc5M1RXcG9ibFozSUFFWUF3PT0%3D&gl=US&hl=en")
- end
- end
-
- describe "#produce_playlist_continuation" do
- it "correctly produces ctoken for requesting index `x` of a playlist" do
- produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D")
-
- produce_playlist_continuation("UCCla9fZca4I7KagBtgRGnOw", 200).should eq("4qmFsgJLEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoSQ0FKNkIxQlVPa05OWjBJJTNEmgIYVVVDbGE5ZlpjYTRJN0thZ0J0Z1JHbk93")
-
- produce_playlist_continuation("PL55713C70BA91BD6E", 100).should eq("4qmFsgJBEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhJQTDU1NzEzQzcwQkE5MUJENkU%3D")
- end
- end
-
- describe "#produce_search_params" do
- it "correctly produces token for searching with specified filters" do
- produce_search_params.should eq("CAASAhABSAA%3D")
-
- produce_search_params(sort: "upload_date", content_type: "video").should eq("CAISAhABSAA%3D")
-
- produce_search_params(content_type: "playlist").should eq("CAASAhADSAA%3D")
-
- produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"]).should eq("CAISCxABIAEwAUgByAEBSAA%3D")
-
- produce_search_params(content_type: "channel").should eq("CAASAhACSAA%3D")
- end
- end
-
- describe "#produce_comment_continuation" do
- it "correctly produces a continuation token for comments" do
- produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
-
- produce_comment_continuation("_cE8xSu6swE", "ADSJ_i1yz21HI4xrtsYXVC-2_kfZ6kx1yjYQumXAAxqH3CAd7ZxKxfLdZS1__fqhCtOASRbbpSBGH_tH1J96Dxux-Qfjk-lUbupMqv08Q3aHzGu7p70VoUMHhI2-GoJpnbpmcOxkGzeIuenRS_ym2Y8fkDowhqLPFgsS0n4djnZ2UmC17F3Ch3N1S1UYf1ZVOc991qOC1iW9kJDzyvRQTWCPsJUPneSaAKW-Rr97pdesOkR4i8cNvHZRnQKe2HEfsvlJOb2C3lF1dJBfJeNfnQYeh5hv6_fZN7bt3-JL1Xk3Qc9NXNxmmbDpwAC_yFR8dthFfUJdyIO9Nu1D79MLYeR-H5HxqUJokkJiGIz4lTE_CXXbhAI").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyiQMK8wJBRFNKX2kxeXoyMUhJNHhydHNZWFZDLTJfa2ZaNmt4MXlqWVF1bVhBQXhxSDNDQWQ3WnhLeGZMZFpTMV9fZnFoQ3RPQVNSYmJwU0JHSF90SDFKOTZEeHV4LVFmamstbFVidXBNcXYwOFEzYUh6R3U3cDcwVm9VTUhoSTItR29KcG5icG1jT3hrR3plSXVlblJTX3ltMlk4ZmtEb3docUxQRmdzUzBuNGRqbloyVW1DMTdGM0NoM04xUzFVWWYxWlZPYzk5MXFPQzFpVzlrSkR6eXZSUVRXQ1BzSlVQbmVTYUFLVy1Scjk3cGRlc09rUjRpOGNOdkhaUm5RS2UySEVmc3ZsSk9iMkMzbEYxZEpCZkplTmZuUVllaDVodjZfZlpON2J0My1KTDFYazNRYzlOWE54bW1iRHB3QUNfeUZSOGR0aEZmVUpkeUlPOU51MUQ3OU1MWWVSLUg1SHhxVUpva2tKaUdJejRsVEVfQ1hYYmhBSSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
-
- produce_comment_continuation("29-q7YnyUmY", "").should eq("EkMSCzI5LXE3WW55VW1ZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iCzI5LXE3WW55VW1ZMAAoFA%3D%3D")
-
- produce_comment_continuation("CvFH_6DNRCY", "").should eq("EkMSC0N2RkhfNkROUkNZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iC0N2RkhfNkROUkNZMAAoFA%3D%3D")
- end
- end
-
- describe "#produce_comment_reply_continuation" do
- it "correctly produces a continuation token for replies to a given comment" do
- produce_comment_reply_continuation("cIHQWOoJeag", "UCq6VFHwMzcMXbuKyG7SQYIg", "Ugx1IP_wGVv3WtGWcdV4AaABAg").should eq("EiYSC2NJSFFXT29KZWFnwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd4MUlQX3dHVnYzV3RHV2NkVjRBYUFCQWciAggAKhhVQ3E2VkZId016Y01YYnVLeUc3U1FZSWcyC2NJSFFXT29KZWFnQAFICg%3D%3D")
-
- produce_comment_reply_continuation("cIHQWOoJeag", "UCq6VFHwMzcMXbuKyG7SQYIg", "Ugza62y_TlmTu9o2RfF4AaABAg").should eq("EiYSC2NJSFFXT29KZWFnwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd6YTYyeV9UbG1UdTlvMlJmRjRBYUFCQWciAggAKhhVQ3E2VkZId016Y01YYnVLeUc3U1FZSWcyC2NJSFFXT29KZWFnQAFICg%3D%3D")
-
- produce_comment_reply_continuation("_cE8xSu6swE", "UC1AZY74-dGVPe6bfxFwwEMg", "UgyBUaRGHB9Jmt1dsUZ4AaABAg").should eq("EiYSC19jRTh4U3U2c3dFwAEByAEB4AEBogINKP___________wFAABgGMk0aSxIaVWd5QlVhUkdIQjlKbXQxZHNVWjRBYUFCQWciAggAKhhVQzFBWlk3NC1kR1ZQZTZiZnhGd3dFTWcyC19jRTh4U3U2c3dFQAFICg%3D%3D")
- end
- end
-
- describe "#produce_channel_community_continuation" do
- it "correctly produces a continuation token for a channel community" do
- produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4").should eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D")
- produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd3cE9NQmVwWEdjclhsUHg2WjRBYUFCQ1FIZGgDKAA%3D").should eq("4qmFsgJmEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTNE")
-
- produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D").should eq("4qmFsgJmEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTNE")
- produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D").should eq("4qmFsgKXFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvoTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUzRA%3D%3D")
- end
- end
-
- describe "#extract_channel_community_cursor" do
- it "correctly extracts a community cursor from a given continuation" do
- extract_channel_community_cursor("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D").should eq("Egljb21tdW5pdHk=")
- extract_channel_community_cursor("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVnd3BPTUJlcFhHY3JYbFB4Nlo0QWFBQkNRKAA=")
-
- extract_channel_community_cursor("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D").should eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVneUUyNjVta1JJNnBPbkttZ2w0QWFBQkNRKAA=")
- extract_channel_community_cursor("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE").should eq("Egljb21tdW5pdHm4AQCqA-kOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIhIcVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1E9PUhkaAMoYw==")
- end
- end
-
- describe "#extract_plid" do
- it "correctly extracts playlist ID from trending URL" do
- extract_plid("/feed/trending?bp=4gIuCggvbS8wNHJsZhIiUExGZ3F1TG5MNTlhbVBud2pLbmNhZUp3MDYzZlU1M3Q0cA%3D%3D").should eq("PLFgquLnL59amPnwjKncaeJw063fU53t4p")
- extract_plid("/feed/trending?bp=4gIvCgkvbS8wYnp2bTISIlBMaUN2Vkp6QnVwS2tDaFNnUDdGWFhDclo2aEp4NmtlTm0%3D").should eq("PLiCvVJzBupKkChSgP7FXXCrZ6hJx6keNm")
- extract_plid("/feed/trending?bp=4gIuCggvbS8wNWpoZxIiUEwzWlE1Q3BOdWxRbUtPUDNJekdsYWN0V1c4dklYX0hFUA%3D%3D").should eq("PL3ZQ5CpNulQmKOP3IzGlactWW8vIX_HEP")
- extract_plid("/feed/trending?bp=4gIuCggvbS8wMnZ4bhIiUEx6akZiYUZ6c21NUnFhdEJnVTdPeGNGTkZhQ2hqTkVERA%3D%3D").should eq("PLzjFbaFzsmMRqatBgU7OxcFNFaChjNEDD")
- end
- end
-
- describe "#sign_token" do
- it "correctly signs a given hash" do
- token = {
- "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
- "expires" => 1554680038,
- "scopes" => [
- ":notifications",
- ":subscriptions/*",
- "GET:tokens*",
- ],
- "signature" => "f__2hS20th8pALF305PJFK-D2aVtvefNnQheILHD2vU=",
- }
- sign_token("SECRET_KEY", token).should eq(token["signature"])
-
- token = {
- "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
- "scopes" => [":notifications", "POST:subscriptions/*"],
- "signature" => "fNvXoT0MRAL9eE6lTE33CEg8HitYJDOL9a22rSN2Ihg=",
- }
- sign_token("SECRET_KEY", token).should eq(token["signature"])
- end
- end
-end
diff --git a/spec/i18next_plurals_spec.cr b/spec/i18next_plurals_spec.cr
new file mode 100644
index 00000000..dcd0f5ec
--- /dev/null
+++ b/spec/i18next_plurals_spec.cr
@@ -0,0 +1,231 @@
+require "spectator"
+require "../src/invidious/helpers/i18next.cr"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+def resolver
+ I18next::Plurals::RESOLVER
+end
+
+FORM_TESTS = {
+ "ach" => I18next::Plurals::PluralForms::Single_gt_one,
+ "ar" => I18next::Plurals::PluralForms::Special_Arabic,
+ "be" => I18next::Plurals::PluralForms::Dual_Slavic,
+ "cy" => I18next::Plurals::PluralForms::Special_Welsh,
+ "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese,
+ "en" => I18next::Plurals::PluralForms::Single_not_one,
+ "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian,
+ "ga" => I18next::Plurals::PluralForms::Special_Irish,
+ "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic,
+ "he" => I18next::Plurals::PluralForms::Special_Hebrew,
+ "hr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian,
+ "is" => I18next::Plurals::PluralForms::Special_Icelandic,
+ "it" => I18next::Plurals::PluralForms::Special_Spanish_Italian,
+ "jv" => I18next::Plurals::PluralForms::Special_Javanese,
+ "kw" => I18next::Plurals::PluralForms::Special_Cornish,
+ "lt" => I18next::Plurals::PluralForms::Special_Lithuanian,
+ "lv" => I18next::Plurals::PluralForms::Special_Latvian,
+ "mk" => I18next::Plurals::PluralForms::Special_Macedonian,
+ "mnk" => I18next::Plurals::PluralForms::Special_Mandinka,
+ "mt" => I18next::Plurals::PluralForms::Special_Maltese,
+ "or" => I18next::Plurals::PluralForms::Special_Odia,
+ "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian,
+ "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese,
+ "pt-PT" => I18next::Plurals::PluralForms::Single_gt_one,
+ "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese,
+ "ro" => I18next::Plurals::PluralForms::Special_Romanian,
+ "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
+ "sl" => I18next::Plurals::PluralForms::Special_Slovenian,
+ "su" => I18next::Plurals::PluralForms::None,
+ "sr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian,
+}
+
+SUFFIX_TESTS = {
+ "ach" => [
+ {num: 0, suffix: ""},
+ {num: 1, suffix: ""},
+ {num: 10, suffix: "_plural"},
+ ],
+ "ar" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_1"},
+ {num: 2, suffix: "_2"},
+ {num: 3, suffix: "_3"},
+ {num: 4, suffix: "_3"},
+ {num: 104, suffix: "_3"},
+ {num: 11, suffix: "_4"},
+ {num: 99, suffix: "_4"},
+ {num: 199, suffix: "_4"},
+ {num: 100, suffix: "_5"},
+ ],
+ "be" => [
+ {num: 0, suffix: "_2"},
+ {num: 1, suffix: "_0"},
+ {num: 5, suffix: "_2"},
+ ],
+ "cy" => [
+ {num: 0, suffix: "_2"},
+ {num: 1, suffix: "_0"},
+ {num: 3, suffix: "_2"},
+ {num: 8, suffix: "_3"},
+ ],
+ "en" => [
+ {num: 0, suffix: "_plural"},
+ {num: 1, suffix: ""},
+ {num: 10, suffix: "_plural"},
+ ],
+ "es" => [
+ {num: 0, suffix: "_2"},
+ {num: 1, suffix: "_0"},
+ {num: 10, suffix: "_2"},
+ {num: 6_000_000, suffix: "_1"},
+ ],
+ "fr" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_0"},
+ {num: 10, suffix: "_2"},
+ {num: 4_000_000, suffix: "_1"},
+ {num: 6_260_000, suffix: "_2"},
+ ],
+ "ga" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 3, suffix: "_2"},
+ {num: 7, suffix: "_3"},
+ {num: 11, suffix: "_4"},
+ ],
+ "gd" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 3, suffix: "_2"},
+ {num: 20, suffix: "_3"},
+ ],
+ "he" => [
+ {num: 0, suffix: "_3"},
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 3, suffix: "_3"},
+ {num: 20, suffix: "_2"},
+ {num: 21, suffix: "_3"},
+ {num: 30, suffix: "_2"},
+ {num: 100, suffix: "_2"},
+ {num: 101, suffix: "_3"},
+ ],
+ "is" => [
+ {num: 1, suffix: ""},
+ {num: 2, suffix: "_plural"},
+ ],
+ "jv" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_1"},
+ ],
+ "kw" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 3, suffix: "_2"},
+ {num: 4, suffix: "_3"},
+ ],
+ "lt" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 10, suffix: "_2"},
+ ],
+ "lv" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 0, suffix: "_2"},
+ ],
+ "mk" => [
+ {num: 1, suffix: ""},
+ {num: 2, suffix: "_plural"},
+ {num: 0, suffix: "_plural"},
+ {num: 11, suffix: "_plural"},
+ {num: 21, suffix: ""},
+ {num: 31, suffix: ""},
+ {num: 311, suffix: "_plural"},
+ ],
+ "mnk" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_1"},
+ {num: 2, suffix: "_2"},
+ ],
+ "mt" => [
+ {num: 1, suffix: "_0"},
+ {num: 2, suffix: "_1"},
+ {num: 11, suffix: "_2"},
+ {num: 20, suffix: "_3"},
+ ],
+ "or" => [
+ {num: 2, suffix: "_1"},
+ {num: 1, suffix: "_0"},
+ ],
+ "pl" => [
+ {num: 0, suffix: "_2"},
+ {num: 1, suffix: "_0"},
+ {num: 5, suffix: "_2"},
+ ],
+ "pt-BR" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_0"},
+ {num: 10, suffix: "_2"},
+ {num: 42, suffix: "_2"},
+ {num: 9_000_000, suffix: "_1"},
+ ],
+ "pt-PT" => [
+ {num: 0, suffix: ""},
+ {num: 1, suffix: ""},
+ {num: 10, suffix: "_plural"},
+ {num: 9_000_000, suffix: "_plural"},
+ ],
+ "ro" => [
+ {num: 0, suffix: "_1"},
+ {num: 1, suffix: "_0"},
+ {num: 20, suffix: "_2"},
+ ],
+ "sk" => [
+ {num: 0, suffix: "_2"},
+ {num: 1, suffix: "_0"},
+ {num: 5, suffix: "_2"},
+ ],
+ "sl" => [
+ {num: 5, suffix: "_0"},
+ {num: 1, suffix: "_1"},
+ {num: 2, suffix: "_2"},
+ {num: 3, suffix: "_3"},
+ ],
+ "su" => [
+ {num: 0, suffix: "_0"},
+ {num: 1, suffix: "_0"},
+ {num: 10, suffix: "_0"},
+ ],
+ "sr" => [
+ {num: 1, suffix: "_0"},
+ {num: 51, suffix: "_0"},
+ {num: 32, suffix: "_1"},
+ {num: 100, suffix: "_2"},
+ {num: 100_000, suffix: "_2"},
+ ],
+}
+
+Spectator.describe "i18next_Plural_Resolver" do
+ describe "get_plural_form" do
+ sample FORM_TESTS do |locale, form|
+ it "returns the right plural form for locale '#{locale}'" do
+ expect(resolver.get_plural_form(locale)).to eq(form)
+ end
+ end
+ end
+
+ describe "get_suffix" do
+ sample SUFFIX_TESTS do |locale, tests|
+ it "returns the right suffix for locale '#{locale}'" do
+ tests.each do |d|
+ expect(resolver.get_suffix(locale, d[:num])).to eq(d[:suffix])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/invidious/hashtag_spec.cr b/spec/invidious/hashtag_spec.cr
new file mode 100644
index 00000000..abc81225
--- /dev/null
+++ b/spec/invidious/hashtag_spec.cr
@@ -0,0 +1,109 @@
+require "../parsers_helper.cr"
+
+Spectator.describe Invidious::Hashtag do
+ it "parses richItemRenderer containers (test 1)" do
+ # Enable mock
+ test_content = load_mock("hashtag/martingarrix_page1")
+ videos, _ = extract_items(test_content)
+
+ expect(typeof(videos)).to eq(Array(SearchItem))
+ expect(videos.size).to eq(60)
+
+ #
+ # Random video check 1
+ #
+ expect(typeof(videos[11])).to eq(SearchItem)
+
+ video_11 = videos[11].as(SearchVideo)
+
+ expect(video_11.id).to eq("06eSsOWcKYA")
+ expect(video_11.title).to eq("Martin Garrix - Live @ Tomorrowland 2018")
+
+ expect(video_11.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
+ expect(video_11.author).to eq("Martin Garrix")
+ expect(video_11.author_verified).to be_true
+
+ expect(video_11.published).to be_close(Time.utc - 3.years, 1.second)
+ expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
+ expect(video_11.views).to eq(40_504_893)
+
+ expect(video_11.badges.live_now?).to be_false
+ expect(video_11.badges.premium?).to be_false
+ expect(video_11.premiere_timestamp).to be_nil
+
+ #
+ # Random video check 2
+ #
+ expect(typeof(videos[35])).to eq(SearchItem)
+
+ video_35 = videos[35].as(SearchVideo)
+
+ expect(video_35.id).to eq("b9HpOAYjY9I")
+ expect(video_35.title).to eq("Martin Garrix feat. Mike Yung - Dreamer (Official Video)")
+
+ expect(video_35.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
+ expect(video_35.author).to eq("Martin Garrix")
+ expect(video_35.author_verified).to be_true
+
+ expect(video_35.published).to be_close(Time.utc - 3.years, 1.second)
+ expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
+ expect(video_35.views).to eq(30_790_049)
+
+ expect(video_35.badges.live_now?).to be_false
+ expect(video_35.badges.premium?).to be_false
+ expect(video_35.premiere_timestamp).to be_nil
+ end
+
+ it "parses richItemRenderer containers (test 2)" do
+ # Enable mock
+ test_content = load_mock("hashtag/martingarrix_page2")
+ videos, _ = extract_items(test_content)
+
+ expect(typeof(videos)).to eq(Array(SearchItem))
+ expect(videos.size).to eq(60)
+
+ #
+ # Random video check 1
+ #
+ expect(typeof(videos[41])).to eq(SearchItem)
+
+ video_41 = videos[41].as(SearchVideo)
+
+ expect(video_41.id).to eq("qhstH17zAjs")
+ expect(video_41.title).to eq("Martin Garrix Radio - Episode 391")
+
+ expect(video_41.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA")
+ expect(video_41.author).to eq("Martin Garrix")
+ expect(video_41.author_verified).to be_true
+
+ expect(video_41.published).to be_close(Time.utc - 2.months, 1.second)
+ expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
+ expect(video_41.views).to eq(63_240)
+
+ expect(video_41.badges.live_now?).to be_false
+ expect(video_41.badges.premium?).to be_false
+ expect(video_41.premiere_timestamp).to be_nil
+
+ #
+ # Random video check 2
+ #
+ expect(typeof(videos[48])).to eq(SearchItem)
+
+ video_48 = videos[48].as(SearchVideo)
+
+ expect(video_48.id).to eq("lqGvW0NIfdc")
+ expect(video_48.title).to eq("Martin Garrix SENTIO Full Album Mix by Sakul")
+
+ expect(video_48.ucid).to eq("UC3833PXeLTS6yRpwGMQpp4Q")
+ expect(video_48.author).to eq("SAKUL")
+ expect(video_48.author_verified).to be_false
+
+ expect(video_48.published).to be_close(Time.utc - 3.weeks, 1.second)
+ expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
+ expect(video_48.views).to eq(68_704)
+
+ expect(video_48.badges.live_now?).to be_false
+ expect(video_48.badges.premium?).to be_false
+ expect(video_48.premiere_timestamp).to be_nil
+ end
+end
diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr
new file mode 100644
index 00000000..9fbb6d6f
--- /dev/null
+++ b/spec/invidious/helpers_spec.cr
@@ -0,0 +1,56 @@
+require "../spec_helper"
+
+CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
+
+Spectator.describe "Helper" do
+ describe "#produce_channel_search_continuation" do
+ it "correctly produces token for searching a specific channel" do
+ expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
+
+ expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0)).to eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
+ end
+ end
+
+ describe "#produce_channel_community_continuation" do
+ it "correctly produces a continuation token for a channel community" do
+ expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4")).to eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D")
+ expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd3cE9NQmVwWEdjclhsUHg2WjRBYUFCQ1FIZGgDKAA%3D")).to eq("4qmFsgJmEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTNE")
+
+ expect(produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqAyQaIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKAA%3D")).to eq("4qmFsgJmEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaSkVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTNE")
+ expect(produce_channel_community_continuation("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "Egljb21tdW5pdHm4AQCqA-cOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIBIaVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1FIZGgDKGM%3D")).to eq("4qmFsgKXFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvoTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUzRA%3D%3D")
+ end
+ end
+
+ describe "#extract_channel_community_cursor" do
+ it "correctly extracts a community cursor from a given continuation" do
+ expect(extract_channel_community_cursor("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D")).to eq("Egljb21tdW5pdHk=")
+ expect(extract_channel_community_cursor("4qmFsgJoEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2QzY0U5TlFtVndXRWRqY2xoc1VIZzJXalJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D")).to eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVnd3BPTUJlcFhHY3JYbFB4Nlo0QWFBQkNRKAA=")
+
+ expect(extract_channel_community_cursor("4qmFsgJoEhhVQy1sSEpaUjNHcXhtMjRfVmRfQUo1WXcaTEVnbGpiMjF0ZFc1cGRIbTRBUUNxQXlRYUlCSWFWV2Q1UlRJMk5XMXJVa2syY0U5dVMyMW5iRFJCWVVGQ1ExRklaR2dES0FBJTI1M0Q%3D")).to eq("Egljb21tdW5pdHm4AQCqAyQaIEhkaAMSGlVneUUyNjVta1JJNnBPbkttZ2w0QWFBQkNRKAA=")
+ expect(extract_channel_community_cursor("4qmFsgKZFBIYVUMtbEhKWlIzR3F4bTI0X1ZkX0FKNVl3GvwTRWdsamIyMXRkVzVwZEhtNEFRQ3FBLWNPQ3NBT1VWVlNWRk5zT1hCTldFWXhZbFZhYmxGWGFHRmlXRkpOVFc1V00xWkhTWGRQVlU1RVdUTm9lRkpXV2xWalJXUkdWVEJPYTFwclRrdGpWVW95V2pCWmVtTkVaSFJQVjJjd1YxaFdiVkp0YUZWUFdGSndWakphVVU0eFRYbE5SV1JhWWxad1NWRlVhM2RsYWs1cFRVVjBkbGw2UWtSVk1scHNXSHBvVkZkVWJIRlNSMG8xWWtSa00xa3lkRWhNVlRWd1dERkNkRmRYT1VoalIwWjZaRU14YmxkVmNFaFVNamt6VW0xc2FHUlhTa1ZpVm1SNlpGaHdkMVF4VG5wT01FNTRUVzVLVWxveFFsQmtNRTVRVTFWV1ZXTXliSGxOYkZadlVWVjBObFZJWkZWaE1WVjVVek5XVW1KSFJsZGtSbXN6VTFkS2QxcFZVbGxWTWtaRlZHMWFWMVpzUm5WVU1HaHNaRmQwVDAxc1ZuZFRiR2QzVGtod2VWZERNVkJUUlVwaFYyNUdOazVZYjNkWU1XUkNWVEZuTWxsdE9EQlBSbWhJVjIwNVdsUXdOVzFZTWpWMVVsVktUV051Y0hOTlNHUjVZMWhLYUZsdFVrVmtibEpZWkcxS2MxRlZhSEZVVjNCd1RrYzVSMXBVUWtWbGJHdzJaSHBTTTJGSVNsQlRTRkpvWWpKR2JWTXdOVEJpVjFweFYycENTVk5XV25aVGFscFJUMFJvY2xWR1ZtaGlhMXA1VkZoc2FXRlRNVkJqUkVaWlYxZFNURmRFUmtaalNIQjBaVWhzZVdGdFJYZE5SMUptVGtoT1dtRkVWbFpUYlZaMVpVVmtSVTFYUmxGaGJVNHhWRVJhYms0d2REVlNTR3hIVTJ4c1QxVkVRbEpYUjFaTFRVaEdNMVV3V2tKVFNFNW9Xa1JXUTJOWFpITmFNRnBxWVcxU1QxWXhaRmxoTURWT1ZVWlNTRlpXVmt0UmVrWlNZVmhvZGxVeFRtMVNWMUV3VFVkc2RXTkVXWGxQVjFZd1VqTmtjR0ZWY0VWVE0wNDBZVVphZG1SWGJISmhibGt5WkZkRmVsTkhXWHBVVjNoTVlVUkNhMkpJUlRGU2JsSjRXbXM0TlUxWGJHdE9NMHBIWWpCR2VVNHhaRkpOTVU1cVlrWkNkMDVyWkU5alYxSnFUMWhHUkdJeU5VNVhhazVUVWxoa2VtRnNVWFJPYkd0NFVXeGtVRTE2WkdGYVJUbHhWR3RhWmxJd2VFaFJiWFJOV1hwQ1dFOUdVak5PTUhCc1lWaHdTMlJ0U2xaa01teEdUVmhDYlZOSVdrZGtWVEZKWTBNeGJGZFlTa1ZaTTBWMFpGUk9XV1J0VmxGbFYzaGhZbFZLTW1WcmVHWlVNR3hPVTJ4YVNsUkZUbEJaTVZwRVVqRndkMVJIWkZoWk1taElZVlZLYWt4VVNtRmFiWGQwVTFSTmVGSkVXa2hsU0doWVRraE9NVTFHWkdoT01qRkNWbFZuTkdORlRsSlhTR3gyV1c1U2NXTlVhSFpYV0d4S1QxZDBUVlJYYzNsUk1XYzBVbTV3VTJKRVZqQmxSR3hwVFc1dk1WUllhRXRrZWxFeFkyMDFTMUpIU21aa2FtaG1UbGhPV21SR1l6UmphazVGVlZka01scFhWbk5SV0VKeVpXNU9kRnBIY0VsalZHaFdZekZzWmt4V2EzZFJWVGt5VFZaVk1tSXlNVE5WZVRGTFZFVXhlRkl3VWxkUmJtYzBWRWRzVGxwR1ZrdGpWbXh6VGtaR2ExVXdhekZhYkUwd1pVaHNSazVXWjNsV1IwWmFZekphZGxZeWFIUlBSRnB6VGpOQ1QxZEhSbkJpTUhoVVZEQmtNbVJ1WkZWT01scHRWbTA1ZFdJd1JURlpWa1p1WWxkS05tSXdNVU5hTW5nMlZHa3hTazU2YkhoWFYzQkpWR3Q0UkZZd1ZsbFVNMDVwVFZjd2VtUkhjM2xVVldONlRWVktjVlJIZEVsTlZXZzFZbXRLUW1WcmJGTlZNbmN6WkVWS1VsSkdPVWxOVldSeVpFUnNiRkpyYUhWWWVrSlhaVWhHYkUxclRUQmxWRTQwWVZVMVQxcEZjR3BWTWtwRlpGTXhXVmRJVGpOVE1uaFdWakp3WVZnelZYUlhiR2N3Wkc1T1NFMXFVWHBZTWxKSFRWaFNWMWt4V2taUk1sWndVMjVPZFZsWFRrZFZlazV3VlVkNGIyRlViRFZTUlZwNFlWaHNiRlJxYkcxYVJXeFlWRlpDTVZGV2JHOU9NRWwzVFc1S1YxSlVWalJrUkVKTFpHNW9ibUpHWkhoU1ZsWTFaV3BqTUZJeWVHbFplbVJJVm1reGVGcEVTbWxhTW5oRlpHeGtjVlJ1U2paTlZFWldVa1JXYW1WSVFsRmtSazVEVm10U1UyUklUbFJhU0doV1pHMDVXRTlWVWtOaFdFWXdUbTFrU0ZSdGIzUk5WMXBOWWxoU2VWSldUbkpoUldoSVZEQjBTVTB5Vmt4VWJGWjJWMVZHTmxKVE1ESmlhbEpaWWtSS2RGRlVWbkpoUlZKNFdtcGplRnB0Y0VSTlIwMDFVbXBrTTJRd05XMVZSWGQ1WVVaQ1psRXdXakZTYkVVelkwZG9SazVJU2taWk1XeFRUV3M1ZDJSWFJuaGlSekZyWWpCVk1tSXhXa0phUnpreVUyeG5lRlpXT1hOaU1EVldXa1V4UkZKNlFqQmpXR2hwVmpCVk1sZFlZM3BUVXpGNFVWY3hhMVJ1WkVKUlZHUnZXVlpGTkdOc1NUQmFWVWwwVW14YWNWZEVUbkpYYXpWTFkyMWFSazlIVm5kUmJXeHFVakIwYmxSRlpFWlZSM042WXpKT2Nsa3dTVE5sVmxaWlZFZGtjRTFZUWtkaU1IQXlaVlUxYUdSVlpGZFZibEpRWVZoYVFsWnRkSFpTYTBVelRGVTFTazFYYUZKUk1VcE1WMmt4U1dKNldreGpXRWt4Wkd0U1RXSnNPVmRVYTBaRlZtcEtabU13VWxGV1YzZ3dVVEowVEZSc2JESmFNMmd4WkZWT1NWa3piRVZPUlVwUlpWVXhNVlJFUW5wT01Wb3dXREkxTVdSclZtbFVNVTU0VGtSa1VrNXJWalZpTUVwUlRVWkdObVI2UmxKU1IyUnhZMVUxZVZnd05UQmpNRGt4V20xNFIyTlVWakJsUmtKR1QxZEdWbUZYZUZKVE1FWllZbGR3UWxWVmJITk9WbWd3WkVSWmRHRkZSbFZpTVd4bVVqRldjMUV5Y0cxV1ZrSlFWMGhrY0ZWUlBUMGFJQklhVldkNVJUSTJOVzFyVWtrMmNFOXVTMjFuYkRSQllVRkNRMUZJWkdnREtHTSUyNTNE")).to eq("Egljb21tdW5pdHm4AQCqA-kOCsAOUVVSVFNsOXBNWEYxYlVablFXaGFiWFJNTW5WM1ZHSXdPVU5EWTNoeFJWWlVjRWRGVTBOa1prTktjVUoyWjBZemNEZHRPV2cwV1hWbVJtaFVPWFJwVjJaUU4xTXlNRWRaYlZwSVFUa3dlak5pTUV0dll6QkRVMlpsWHpoVFdUbHFSR0o1YkRkM1kydEhMVTVwWDFCdFdXOUhjR0Z6ZEMxbldVcEhUMjkzUm1saGRXSkViVmR6ZFhwd1QxTnpOME54TW5KUloxQlBkME5QU1VWVWMybHlNbFZvUVV0NlVIZFVhMVV5UzNWUmJHRldkRmszU1dKd1pVUllVMkZFVG1aV1ZsRnVUMGhsZFd0T01sVndTbGd3TkhweVdDMVBTRUphV25GNk5Yb3dYMWRCVTFnMlltODBPRmhIV205WlQwNW1YMjV1UlVKTWNucHNNSGR5Y1hKaFltUkVkblJYZG1Kc1FVaHFUV3BwTkc5R1pUQkVlbGw2ZHpSM2FISlBTSFJoYjJGbVMwNTBiV1pxV2pCSVNWWnZTalpRT0RoclVGVmhia1p5VFhsaWFTMVBjREZZV1dSTFdERkZjSHB0ZUhseWFtRXdNR1JmTkhOWmFEVlZTbVZ1ZUVkRU1XRlFhbU4xVERabk4wdDVSSGxHU2xsT1VEQlJXR1ZLTUhGM1UwWkJTSE5oWkRWQ2NXZHNaMFpqYW1ST1YxZFlhMDVOVUZSSFZWVktRekZSYVhodlUxTm1SV1EwTUdsdWNEWXlPV1YwUjNkcGFVcEVTM040YUZadmRXbHJhblkyZFdFelNHWXpUV3hMYURCa2JIRTFSblJ4Wms4NU1XbGtOM0pHYjBGeU4xZFJNMU5qYkZCd05rZE9jV1JqT1hGRGIyNU5Xak5TUlhkemFsUXRObGt4UWxkUE16ZGFaRTlxVGtaZlIweEhRbXRNWXpCWE9GUjNOMHBsYVhwS2RtSlZkMmxGTVhCbVNIWkdkVTFJY0MxbFdYSkVZM0V0ZFROWWRtVlFlV3hhYlVKMmVreGZUMGxOU2xaSlRFTlBZMVpEUjFwd1RHZFhZMmhIYVVKakxUSmFabXd0U1RNeFJEWkhlSGhYTkhOMU1GZGhOMjFCVlVnNGNFTlJXSGx2WW5ScWNUaHZXWGxKT1d0TVRXc3lRMWc0Um5wU2JEVjBlRGxpTW5vMVRYaEtkelExY201S1JHSmZkamhmTlhOWmRGYzRjak5FVVdkMlpXVnNRWEJyZW5OdFpHcEljVGhWYzFsZkxWa3dRVTkyTVZVMmIyMTNVeTFLVEUxeFIwUldRbmc0VEdsTlpGVktjVmxzTkZGa1UwazFabE0wZUhsRk5WZ3lWR0ZaYzJadlYyaHRPRFpzTjNCT1dHRnBiMHhUVDBkMmRuZFVOMlptVm05dWIwRTFZVkZuYldKNmIwMUNaMng2VGkxSk56bHhXV3BJVGt4RFYwVllUM05pTVcwemRHc3lUVWN6TVVKcVRHdElNVWg1YmtKQmVrbFNVMnczZEVKUlJGOUlNVWRyZERsbFJraHVYekJXZUhGbE1rTTBlVE40YVU1T1pFcGpVMkpFZFMxWVdITjNTMnhWVjJwYVgzVXRXbGcwZG5OSE1qUXpYMlJHTVhSV1kxWkZRMlZwU25OdVlXTkdVek5wVUd4b2FUbDVSRVp4YVhsbFRqbG1aRWxYVFZCMVFWbG9OMEl3TW5KV1JUVjRkREJLZG5obmJGZHhSVlY1ZWpjMFIyeGlZemRIVmkxeFpESmlaMnhFZGxkcVRuSjZNVEZWUkRWamVIQlFkRk5DVmtSU2RITlRaSGhWZG05WE9VUkNhWEYwTm1kSFRtb3RNV1pNYlhSeVJWTnJhRWhIVDB0SU0yVkxUbFZ2V1VGNlJTMDJialJZYkRKdFFUVnJhRVJ4WmpjeFptcERNR001UmpkM2QwNW1VRXd5YUZCZlEwWjFSbEUzY0doRk5ISkZZMWxTTWs5d2RXRnhiRzFrYjBVMmIxWkJaRzkyU2xneFZWOXNiMDVWWkUxRFJ6QjBjWGhpVjBVMldYY3pTUzF4UVcxa1RuZEJRVGRvWVZFNGNsSTBaVUl0UmxacVdETnJXazVLY21aRk9HVndRbWxqUjB0blRFZEZVR3N6YzJOclkwSTNlVlZZVEdkcE1YQkdiMHAyZVU1aGRVZFdVblJQYVhaQlZtdHZSa0UzTFU1Sk1XaFJRMUpMV2kxSWJ6WkxjWEkxZGtSTWJsOVdUa0ZFVmpKZmMwUlFWV3gwUTJ0TFRsbDJaM2gxZFVOSVkzbEVORUpRZVUxMVREQnpOMVowWDI1MWRrVmlUMU54TkRkUk5rVjViMEpRTUZGNmR6RlJSR2RxY1U1eVgwNTBjMDkxWm14R2NUVjBlRkJGT1dGVmFXeFJTMEZYYldwQlVVbHNOVmgwZERZdGFFRlViMWxmUjFWc1EycG1WVkJQV0hkcFVRPT0aIhIcVWd5RTI2NW1rUkk2cE9uS21nbDRBYUFCQ1E9PUhkaAMoYw==")
+ end
+ end
+
+ describe "#sign_token" do
+ it "correctly signs a given hash" do
+ token = {
+ "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "expires" => 1554680038,
+ "scopes" => [
+ ":notifications",
+ ":subscriptions/*",
+ "GET:tokens*",
+ ],
+ "signature" => "f__2hS20th8pALF305PJFK-D2aVtvefNnQheILHD2vU=",
+ }
+ expect(sign_token("SECRET_KEY", token)).to eq(token["signature"])
+
+ token = {
+ "session" => "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "scopes" => [":notifications", "POST:subscriptions/*"],
+ "signature" => "fNvXoT0MRAL9eE6lTE33CEg8HitYJDOL9a22rSN2Ihg=",
+ }
+ expect(sign_token("SECRET_KEY", token)).to eq(token["signature"])
+ end
+ end
+end
diff --git a/spec/invidious/search/iv_filters_spec.cr b/spec/invidious/search/iv_filters_spec.cr
new file mode 100644
index 00000000..3cefafa1
--- /dev/null
+++ b/spec/invidious/search/iv_filters_spec.cr
@@ -0,0 +1,370 @@
+require "../../../src/invidious/search/filters"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+FEATURES_TEXT = {
+ Invidious::Search::Filters::Features::Live => "live",
+ Invidious::Search::Filters::Features::FourK => "4k",
+ Invidious::Search::Filters::Features::HD => "hd",
+ Invidious::Search::Filters::Features::Subtitles => "subtitles",
+ Invidious::Search::Filters::Features::CCommons => "commons",
+ Invidious::Search::Filters::Features::ThreeSixty => "360",
+ Invidious::Search::Filters::Features::VR180 => "vr180",
+ Invidious::Search::Filters::Features::ThreeD => "3d",
+ Invidious::Search::Filters::Features::HDR => "hdr",
+ Invidious::Search::Filters::Features::Location => "location",
+ Invidious::Search::Filters::Features::Purchased => "purchased",
+}
+
+Spectator.describe Invidious::Search::Filters do
+ # -------------------
+ # Decode (legacy)
+ # -------------------
+
+ describe "#from_legacy_filters" do
+ it "Decodes channel: filter" do
+ query = "test channel:UC123456 request"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("UC123456")
+ expect(qury).to eq("test request")
+ expect(subs).to be_false
+ end
+
+ it "Decodes user: filter" do
+ query = "user:LinusTechTips broke something (again)"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("LinusTechTips")
+ expect(qury).to eq("broke something (again)")
+ expect(subs).to be_false
+ end
+
+ it "Decodes type: filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ query = "Eiffel 65 - Blue [1 Hour] type:#{value}"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(type: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("Eiffel 65 - Blue [1 Hour]")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes content_type: filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ query = "I like to watch content_type:#{value}"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(type: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("I like to watch")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes date: filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ query = "This date:#{value} is old!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(date: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("This is old!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes duration: filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ query = "This duration:#{value} is old!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(duration: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("This is old!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes feature: filter" do
+ Invidious::Search::Filters::Features.each do |value|
+ string = FEATURES_TEXT[value]
+ query = "I like my precious feature:#{string} ^^"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(features: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("I like my precious ^^")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes features: filter" do
+ query = "This search has many features:vr180,cc,hdr :o"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons)
+
+ expect(fltr).to eq(described_class.new(features: features))
+ expect(chan).to eq("")
+ expect(qury).to eq("This search has many :o")
+ expect(subs).to be_false
+ end
+
+ it "Decodes sort: filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ query = "Computer? sort:#{value} my files!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(sort: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("Computer? my files!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes subscriptions: filter" do
+ query = "enable subscriptions:true"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("enable")
+ expect(subs).to be_true
+ end
+
+ it "Ignores junk data" do
+ query = "duration:I sort:like type:cleaning features:stuff date:up!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("")
+ expect(subs).to be_false
+ end
+
+ it "Keeps unknown keys" do
+ query = "to:be or:not to:be"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("to:be or:not to:be")
+ expect(subs).to be_false
+ end
+ end
+
+ # -------------------
+ # Decode (URL)
+ # -------------------
+
+ describe "#from_iv_params" do
+ it "Decodes type= filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ params = HTTP::Params.parse("type=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(type: value))
+ end
+ end
+
+ it "Decodes date= filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ params = HTTP::Params.parse("date=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(date: value))
+ end
+ end
+
+ it "Decodes duration= filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ params = HTTP::Params.parse("duration=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(duration: value))
+ end
+ end
+
+ it "Decodes features= filter (single)" do
+ Invidious::Search::Filters::Features.each do |value|
+ string = described_class.format_features(value)
+ params = HTTP::Params.parse("features=#{string}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: value))
+ end
+ end
+
+ it "Decodes features= filter (multiple - comma separated)" do
+ features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons)
+ params = HTTP::Params.parse("features=vr180%2Ccc%2Chdr") # %2C is a comma
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: features))
+ end
+
+ it "Decodes features= filter (multiple - URL parameters)" do
+ features = Invidious::Search::Filters::Features.flags(ThreeSixty, HD, FourK)
+ params = HTTP::Params.parse("features=4k&features=360&features=hd")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: features))
+ end
+
+ it "Decodes sort= filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ params = HTTP::Params.parse("sort=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(sort: value))
+ end
+ end
+
+ it "Ignores junk data" do
+ params = HTTP::Params.parse("foo=bar&sort=views&answer=42&type=channel")
+
+ expect(described_class.from_iv_params(params)).to eq(
+ described_class.new(
+ sort: Invidious::Search::Filters::Sort::Views,
+ type: Invidious::Search::Filters::Type::Channel
+ )
+ )
+ end
+ end
+
+ # -------------------
+ # Encode (URL)
+ # -------------------
+
+ describe "#to_iv_params" do
+ it "Encodes date filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ filters = described_class.new(date: value)
+ params = filters.to_iv_params
+
+ if value.none?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("date=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes type filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ filters = described_class.new(type: value)
+ params = filters.to_iv_params
+
+ if value.all?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("type=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes duration filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ filters = described_class.new(duration: value)
+ params = filters.to_iv_params
+
+ if value.none?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("duration=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes features filter (single)" do
+ Invidious::Search::Filters::Features.each do |value|
+ filters = described_class.new(features: value)
+
+ expect("#{filters.to_iv_params}")
+ .to eq("features=" + FEATURES_TEXT[value])
+ end
+ end
+
+ it "Encodes features filter (multiple)" do
+ features = Invidious::Search::Filters::Features.flags(Subtitles, Live, ThreeSixty)
+ filters = described_class.new(features: features)
+
+ expect("#{filters.to_iv_params}")
+ .to eq("features=live%2Csubtitles%2C360") # %2C is a comma
+ end
+
+ it "Encodes sort filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ filters = described_class.new(sort: value)
+ params = filters.to_iv_params
+
+ if value.relevance?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("sort=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes multiple filters" do
+ filters = described_class.new(
+ date: Invidious::Search::Filters::Date::Today,
+ duration: Invidious::Search::Filters::Duration::Medium,
+ features: Invidious::Search::Filters::Features.flags(Location, Purchased),
+ sort: Invidious::Search::Filters::Sort::Relevance
+ )
+
+ params = filters.to_iv_params
+
+ # Check the `date` param
+ expect(params).to have_key("date")
+ expect(params.fetch_all("date")).to contain_exactly("today")
+
+ # Check the `type` param
+ expect(params).to_not have_key("type")
+ expect(params["type"]?).to be_nil
+
+ # Check the `duration` param
+ expect(params).to have_key("duration")
+ expect(params.fetch_all("duration")).to contain_exactly("medium")
+
+ # Check the `features` param
+ expect(params).to have_key("features")
+ expect(params.fetch_all("features")).to contain_exactly("location,purchased")
+
+ # Check the `sort` param
+ expect(params).to_not have_key("sort")
+ expect(params["sort"]?).to be_nil
+
+ # Check if there aren't other parameters
+ params.delete("date")
+ params.delete("duration")
+ params.delete("features")
+
+ expect(params).to be_empty
+ end
+ end
+end
diff --git a/spec/invidious/search/query_spec.cr b/spec/invidious/search/query_spec.cr
new file mode 100644
index 00000000..063b69f1
--- /dev/null
+++ b/spec/invidious/search/query_spec.cr
@@ -0,0 +1,242 @@
+require "../../../src/invidious/search/filters"
+require "../../../src/invidious/search/query"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+Spectator.describe Invidious::Search::Query do
+ describe Type::Regular do
+ # -------------------
+ # Query parsing
+ # -------------------
+
+ it "parses query with URL prameters (q)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=What+is+Love+10+hour&type=video&duration=long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("What is Love 10 hour")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with URL prameters (search_query)" do
+ query = described_class.new(
+ HTTP::Params.parse("search_query=What+is+Love+10+hour&type=video&duration=long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("What is Love 10 hour")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with legacy filters (q)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Nyan+cat+duration:long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Nyan cat")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with legacy filters (search_query)" do
+ query = described_class.new(
+ HTTP::Params.parse("search_query=Nyan+cat+duration:long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Nyan cat")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with both URL params and legacy filters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Vamos+a+la+playa+duration:long&type=Video&date=year"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Vamos a la playa duration:long")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+
+ # -------------------
+ # Type switching
+ # -------------------
+
+ it "switches to channel search (URL param)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=thunderbolt+4&channel=UC0vBXGSyV14uvJ4hECDOl0Q"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to eq("UC0vBXGSyV14uvJ4hECDOl0Q")
+ expect(query.text).to eq("thunderbolt 4")
+ expect(query.filters.default?).to be_true
+ end
+
+ it "switches to channel search (legacy)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=channel%3AUCRPdsCVuH53rcbTcEkuY4uQ+rdna3"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to eq("UCRPdsCVuH53rcbTcEkuY4uQ")
+ expect(query.text).to eq("rdna3")
+ expect(query.filters.default?).to be_true
+ end
+
+ it "switches to subscriptions search" do
+ query = described_class.new(
+ HTTP::Params.parse("q=subscriptions:true+tunak+tunak+tun"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("tunak tunak tun")
+ expect(query.filters.default?).to be_true
+ end
+ end
+
+ describe Type::Channel do
+ it "ignores extra parameters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Take+on+me+channel%3AUC12345679&type=video&date=year"),
+ Invidious::Search::Query::Type::Channel, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Take on me")
+ expect(query.filters.default?).to be_true
+ end
+ end
+
+ describe Type::Subscriptions do
+ it "works" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Harlem+shake&type=video&date=year"),
+ Invidious::Search::Query::Type::Subscriptions, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Harlem shake")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+ end
+
+ describe Type::Playlist do
+ it "ignores extra parameters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Harlem+shake+type:video+date:year&channel=UC12345679"),
+ Invidious::Search::Query::Type::Playlist, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Playlist)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Harlem shake")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+ end
+
+ describe "#to_http_params" do
+ it "formats regular search" do
+ query = described_class.new(
+ HTTP::Params.parse("q=The+Simpsons+hiding+in+bush&duration=short"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ params = query.to_http_params
+
+ expect(params).to have_key("duration")
+ expect(params["duration"]?).to eq("short")
+
+ expect(params).to have_key("q")
+ expect(params["q"]?).to eq("The Simpsons hiding in bush")
+
+ # Check if there aren't other parameters
+ params.delete("duration")
+ params.delete("q")
+ expect(params).to be_empty
+ end
+
+ it "formats channel search" do
+ query = described_class.new(
+ HTTP::Params.parse("q=channel:UC2DjFE7Xf11URZqWBigcVOQ%20multimeter"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ params = query.to_http_params
+
+ expect(params).to have_key("channel")
+ expect(params["channel"]?).to eq("UC2DjFE7Xf11URZqWBigcVOQ")
+
+ expect(params).to have_key("q")
+ expect(params["q"]?).to eq("multimeter")
+
+ # Check if there aren't other parameters
+ params.delete("channel")
+ params.delete("q")
+ expect(params).to be_empty
+ end
+ end
+end
diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr
new file mode 100644
index 00000000..8abed5ce
--- /dev/null
+++ b/spec/invidious/search/yt_filters_spec.cr
@@ -0,0 +1,143 @@
+require "../../../src/invidious/search/filters"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+# Encoded filter values are extracted from the search
+# page of Youtube with any browser devtools HTML inspector.
+
+DATE_FILTERS = {
+ Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D",
+ Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D",
+ Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D",
+ Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D",
+ Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D",
+}
+
+TYPE_FILTERS = {
+ Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D",
+ Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D",
+ Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D",
+ Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D",
+}
+
+DURATION_FILTERS = {
+ Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D",
+ Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D",
+ Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D",
+}
+
+FEATURE_FILTERS = {
+ Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D",
+ Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D",
+ Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D",
+ Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D",
+ Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D",
+}
+
+SORT_FILTERS = {
+ Invidious::Search::Filters::Sort::Relevance => "8AEB",
+ Invidious::Search::Filters::Sort::Date => "CALwAQE%3D",
+ Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D",
+ Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D",
+}
+
+Spectator.describe Invidious::Search::Filters do
+ # -------------------
+ # Encode YT params
+ # -------------------
+
+ describe "#to_yt_params" do
+ sample DATE_FILTERS do |value, result|
+ it "Encodes upload date filter '#{value}'" do
+ expect(described_class.new(date: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample TYPE_FILTERS do |value, result|
+ it "Encodes content type filter '#{value}'" do
+ expect(described_class.new(type: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample DURATION_FILTERS do |value, result|
+ it "Encodes duration filter '#{value}'" do
+ expect(described_class.new(duration: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample FEATURE_FILTERS do |value, result|
+ it "Encodes feature filter '#{value}'" do
+ expect(described_class.new(features: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample SORT_FILTERS do |value, result|
+ it "Encodes sort filter '#{value}'" do
+ expect(described_class.new(sort: value).to_yt_params).to eq(result)
+ end
+ end
+ end
+
+ # -------------------
+ # Decode YT params
+ # -------------------
+
+ describe "#from_yt_params" do
+ sample DATE_FILTERS do |value, encoded|
+ it "Decodes upload date filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(date: value))
+ end
+ end
+
+ sample TYPE_FILTERS do |value, encoded|
+ it "Decodes content type filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(type: value))
+ end
+ end
+
+ sample DURATION_FILTERS do |value, encoded|
+ it "Decodes duration filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(duration: value))
+ end
+ end
+
+ sample FEATURE_FILTERS do |value, encoded|
+ it "Decodes feature filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(features: value))
+ end
+ end
+
+ sample SORT_FILTERS do |value, encoded|
+ it "Decodes sort filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(sort: value))
+ end
+ end
+ end
+end
diff --git a/spec/invidious/user/imports_spec.cr b/spec/invidious/user/imports_spec.cr
new file mode 100644
index 00000000..762ce0d8
--- /dev/null
+++ b/spec/invidious/user/imports_spec.cr
@@ -0,0 +1,51 @@
+require "spectator"
+require "../../../src/invidious/user/imports"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+def csv_sample
+ return <<-CSV
+ Kanal-ID,Kanal-URL,Kanaltitel
+ UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla
+ UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie
+ UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft
+ UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic
+ UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott
+ UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun
+ UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations
+ UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos
+ UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake
+ UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy
+ UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp
+ UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips
+ UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord
+ CSV
+end
+
+Spectator.describe Invidious::User::Import do
+ it "imports CSV" do
+ subscriptions = Invidious::User::Import.parse_subscription_export_csv(csv_sample)
+
+ expect(subscriptions).to be_an(Array(String))
+ expect(subscriptions.size).to eq(13)
+
+ expect(subscriptions).to contain_exactly(
+ "UC0hHW5Y08ggq-9kbrGgWj0A",
+ "UC0vBXGSyV14uvJ4hECDOl0Q",
+ "UC1sELGmy5jp5fQUugmuYlXQ",
+ "UC9kFnwdCRrX7oTjqKd6-tiQ",
+ "UCBa659QWEk1AI4Tg--mrJ2A",
+ "UCGu6_XQ64rXPR6nuitMQE_A",
+ "UCGwu0nbY2wSkW8N-cghnLpA",
+ "UCQ0OvZ54pCFZwsKxbltg_tg",
+ "UCRE6itj4Jte4manQEu3Y7OA",
+ "UCRLc6zsv_d0OEBO8OOkz-DA",
+ "UCSl5Uxu2LyaoAoMMGp6oTJA",
+ "UCXuqSBlHAE6Xw-yeJA0Tunw",
+ "UCZ5XnGb-3t7jCkXdawN2tkA",
+ ).in_order
+ end
+end
diff --git a/spec/invidious/utils_spec.cr b/spec/invidious/utils_spec.cr
new file mode 100644
index 00000000..7c2c2711
--- /dev/null
+++ b/spec/invidious/utils_spec.cr
@@ -0,0 +1,46 @@
+require "../spec_helper"
+
+Spectator.describe "Utils" do
+ describe "decode_date" do
+ it "parses short dates (en-US)" do
+ expect(decode_date("1s ago")).to be_close(Time.utc - 1.second, 500.milliseconds)
+ expect(decode_date("2min ago")).to be_close(Time.utc - 2.minutes, 500.milliseconds)
+ expect(decode_date("3h ago")).to be_close(Time.utc - 3.hours, 500.milliseconds)
+ expect(decode_date("4d ago")).to be_close(Time.utc - 4.days, 500.milliseconds)
+ expect(decode_date("5w ago")).to be_close(Time.utc - 5.weeks, 500.milliseconds)
+ expect(decode_date("6mo ago")).to be_close(Time.utc - 6.months, 500.milliseconds)
+ expect(decode_date("7y ago")).to be_close(Time.utc - 7.years, 500.milliseconds)
+ end
+
+ it "parses short dates (en-GB)" do
+ expect(decode_date("55s ago")).to be_close(Time.utc - 55.seconds, 500.milliseconds)
+ expect(decode_date("44min ago")).to be_close(Time.utc - 44.minutes, 500.milliseconds)
+ expect(decode_date("22hr ago")).to be_close(Time.utc - 22.hours, 500.milliseconds)
+ expect(decode_date("1day ago")).to be_close(Time.utc - 1.day, 500.milliseconds)
+ expect(decode_date("2days ago")).to be_close(Time.utc - 2.days, 500.milliseconds)
+ expect(decode_date("3wk ago")).to be_close(Time.utc - 3.weeks, 500.milliseconds)
+ expect(decode_date("11mo ago")).to be_close(Time.utc - 11.months, 500.milliseconds)
+ expect(decode_date("11yr ago")).to be_close(Time.utc - 11.years, 500.milliseconds)
+ end
+
+ it "parses long forms (singular)" do
+ expect(decode_date("1 second ago")).to be_close(Time.utc - 1.second, 500.milliseconds)
+ expect(decode_date("1 minute ago")).to be_close(Time.utc - 1.minute, 500.milliseconds)
+ expect(decode_date("1 hour ago")).to be_close(Time.utc - 1.hour, 500.milliseconds)
+ expect(decode_date("1 day ago")).to be_close(Time.utc - 1.day, 500.milliseconds)
+ expect(decode_date("1 week ago")).to be_close(Time.utc - 1.week, 500.milliseconds)
+ expect(decode_date("1 month ago")).to be_close(Time.utc - 1.month, 500.milliseconds)
+ expect(decode_date("1 year ago")).to be_close(Time.utc - 1.year, 500.milliseconds)
+ end
+
+ it "parses long forms (plural)" do
+ expect(decode_date("5 seconds ago")).to be_close(Time.utc - 5.seconds, 500.milliseconds)
+ expect(decode_date("17 minutes ago")).to be_close(Time.utc - 17.minutes, 500.milliseconds)
+ expect(decode_date("23 hours ago")).to be_close(Time.utc - 23.hours, 500.milliseconds)
+ expect(decode_date("3 days ago")).to be_close(Time.utc - 3.days, 500.milliseconds)
+ expect(decode_date("2 weeks ago")).to be_close(Time.utc - 2.weeks, 500.milliseconds)
+ expect(decode_date("9 months ago")).to be_close(Time.utc - 9.months, 500.milliseconds)
+ expect(decode_date("8 years ago")).to be_close(Time.utc - 8.years, 500.milliseconds)
+ end
+ end
+end
diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr
new file mode 100644
index 00000000..f96703f6
--- /dev/null
+++ b/spec/invidious/videos/regular_videos_extract_spec.cr
@@ -0,0 +1,168 @@
+require "../../parsers_helper.cr"
+
+Spectator.describe "parse_video_info" do
+ it "parses a regular video" do
+ # Enable mock
+ _player = load_mock("video/regular_mrbeast.player")
+ _next = load_mock("video/regular_mrbeast.next")
+
+ raw_data = _player.merge!(_next)
+ info = parse_video_info("2isYuQZMbdU", raw_data)
+
+ # Some basic verifications
+ expect(typeof(info)).to eq(Hash(String, JSON::Any))
+
+ expect(info["videoType"].as_s).to eq("Video")
+
+ # Basic video infos
+
+ expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
+ expect(info["views"].as_i).to eq(220_226_287)
+ expect(info["likes"].as_i).to eq(6_870_691)
+
+ # For some reason the video length from VideoDetails and the
+ # one from microformat differs by 1s...
+ expect(info["lengthSeconds"].as_i).to be_between(930_i64, 931_i64)
+
+ expect(info["published"].as_s).to eq("2022-08-04T00:00:00Z")
+
+ # Extra video infos
+
+ expect(info["allowedRegions"].as_a).to_not be_empty
+ expect(info["allowedRegions"].as_a.size).to eq(249)
+
+ expect(info["allowedRegions"].as_a).to contain(
+ "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR",
+ "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU",
+ "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"
+ )
+
+ expect(info["keywords"].as_a).to be_empty
+
+ expect(info["allowRatings"].as_bool).to be_true
+ expect(info["isFamilyFriendly"].as_bool).to be_true
+ expect(info["isListed"].as_bool).to be_true
+ expect(info["isUpcoming"].as_bool).to be_false
+
+ # Related videos
+
+ expect(info["relatedVideos"].as_a.size).to eq(20)
+
+ expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
+ expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
+ expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
+ expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
+ expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
+ expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
+ expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
+
+ # Description
+
+ description = "🚀Launch a store on Shopify, I’ll buy from 100 random stores that do ▸ "
+
+ expect(info["description"].as_s).to start_with(description)
+ expect(info["shortDescription"].as_s).to start_with(description)
+ expect(info["descriptionHtml"].as_s).to start_with(description)
+
+ # Video metadata
+
+ expect(info["genre"].as_s).to eq("Entertainment")
+ expect(info["genreUcid"].as_s?).to be_nil
+ expect(info["license"].as_s).to be_empty
+
+ # Author infos
+
+ expect(info["author"].as_s).to eq("MrBeast")
+ expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
+
+ expect(info["authorThumbnail"].as_s).to eq(
+ "https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj"
+ )
+
+ expect(info["authorVerified"].as_bool).to be_true
+ expect(info["subCountText"].as_s).to eq("320M")
+ end
+
+ it "parses a regular video with no descrition/comments" do
+ # Enable mock
+ _player = load_mock("video/regular_no-description.player")
+ _next = load_mock("video/regular_no-description.next")
+
+ raw_data = _player.merge!(_next)
+ info = parse_video_info("iuevw6218F0", raw_data)
+
+ # Some basic verifications
+ expect(typeof(info)).to eq(Hash(String, JSON::Any))
+
+ expect(info["videoType"].as_s).to eq("Video")
+
+ # Basic video infos
+
+ expect(info["title"].as_s).to eq("Chris Rea - Auberge")
+ expect(info["views"].as_i).to eq(14_324_584)
+ expect(info["likes"].as_i).to eq(35_870)
+ expect(info["lengthSeconds"].as_i).to eq(283_i64)
+ expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
+
+ # Extra video infos
+
+ expect(info["allowedRegions"].as_a).to_not be_empty
+ expect(info["allowedRegions"].as_a.size).to eq(249)
+
+ expect(info["allowedRegions"].as_a).to contain(
+ "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR",
+ "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU",
+ "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"
+ )
+
+ expect(info["keywords"].as_a).to_not be_empty
+ expect(info["keywords"].as_a.size).to eq(4)
+
+ expect(info["keywords"].as_a).to contain_exactly(
+ "Chris",
+ "Rea",
+ "Auberge",
+ "1991"
+ ).in_any_order
+
+ expect(info["allowRatings"].as_bool).to be_true
+ expect(info["isFamilyFriendly"].as_bool).to be_true
+ expect(info["isListed"].as_bool).to be_true
+ expect(info["isUpcoming"].as_bool).to be_false
+
+ # Related videos
+
+ expect(info["relatedVideos"].as_a.size).to eq(20)
+
+ expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4")
+ expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
+ expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
+ expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
+ expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
+ expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
+ expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
+
+ # Description
+
+ expect(info["description"].as_s).to eq(" ")
+ expect(info["shortDescription"].as_s).to be_empty
+ expect(info["descriptionHtml"].as_s).to eq("")
+
+ # Video metadata
+
+ expect(info["genre"].as_s).to eq("Music")
+ expect(info["genreUcid"].as_s?).to be_nil
+ expect(info["license"].as_s).to be_empty
+
+ # Author infos
+
+ expect(info["author"].as_s).to eq("ChrisReaVideos")
+ expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
+
+ expect(info["authorThumbnail"].as_s).to eq(
+ "https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
+ )
+ expect(info["authorVerified"].as_bool).to be_false
+ expect(info["subCountText"].as_s).to eq("3.11K")
+ end
+end
diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr
new file mode 100644
index 00000000..c3a9b228
--- /dev/null
+++ b/spec/invidious/videos/scheduled_live_extract_spec.cr
@@ -0,0 +1,111 @@
+require "../../parsers_helper.cr"
+
+Spectator.describe "parse_video_info" do
+ it "parses scheduled livestreams data" do
+ # Enable mock
+ _player = load_mock("video/scheduled_live_PBD-Podcast.player")
+ _next = load_mock("video/scheduled_live_PBD-Podcast.next")
+
+ raw_data = _player.merge!(_next)
+ info = parse_video_info("N-yVic7BbY0", raw_data)
+
+ # Some basic verifications
+ expect(typeof(info)).to eq(Hash(String, JSON::Any))
+
+ expect(info["videoType"].as_s).to eq("Scheduled")
+
+ # Basic video infos
+
+ expect(info["title"].as_s).to eq("Home Team | PBD Podcast | Ep. 241")
+ expect(info["views"].as_i).to eq(6)
+ expect(info["likes"].as_i).to eq(7)
+ expect(info["lengthSeconds"].as_i).to eq(0_i64)
+ expect(info["published"].as_s).to eq("2023-02-28T14:00:00Z") # Unix 1677592800
+
+ # Extra video infos
+
+ expect(info["allowedRegions"].as_a).to_not be_empty
+ expect(info["allowedRegions"].as_a.size).to eq(249)
+
+ expect(info["allowedRegions"].as_a).to contain(
+ "AD", "AR", "BA", "BT", "CZ", "FO", "GL", "IO", "KE", "KH", "LS",
+ "LT", "MP", "NO", "PR", "RO", "SE", "SK", "SS", "SX", "SZ", "ZW"
+ )
+
+ expect(info["keywords"].as_a).to_not be_empty
+ expect(info["keywords"].as_a.size).to eq(25)
+
+ expect(info["keywords"].as_a).to contain_exactly(
+ "Patrick Bet-David",
+ "Valeutainment",
+ "The BetDavid Podcast",
+ "The BetDavid Show",
+ "Betdavid",
+ "PBD",
+ "BetDavid show",
+ "Betdavid podcast",
+ "podcast betdavid",
+ "podcast patrick",
+ "patrick bet david podcast",
+ "Valuetainment podcast",
+ "Entrepreneurs",
+ "Entrepreneurship",
+ "Entrepreneur Motivation",
+ "Entrepreneur Advice",
+ "Startup Entrepreneurs",
+ "valuetainment",
+ "patrick bet david",
+ "PBD podcast",
+ "Betdavid show",
+ "Betdavid Podcast",
+ "Podcast Betdavid",
+ "Show Betdavid",
+ "PBDPodcast"
+ ).in_any_order
+
+ expect(info["allowRatings"].as_bool).to be_true
+ expect(info["isFamilyFriendly"].as_bool).to be_true
+ expect(info["isListed"].as_bool).to be_true
+ expect(info["isUpcoming"].as_bool).to be_true
+
+ # Related videos
+
+ expect(info["relatedVideos"].as_a.size).to eq(20)
+
+ expect(info["relatedVideos"][0]["id"]).to eq("j7jPzzjbVuk")
+ expect(info["relatedVideos"][0]["author"]).to eq("Democracy Now!")
+ expect(info["relatedVideos"][0]["ucid"]).to eq("UCzuqE7-t13O4NIDYJfakrhw")
+ expect(info["relatedVideos"][0]["view_count"]).to eq("7576")
+ expect(info["relatedVideos"][0]["short_view_count"]).to eq("7.5K")
+ expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
+
+ # Description
+
+ description_start_text = "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - https://aura.com/pbd"
+
+ expect(info["description"].as_s).to start_with(description_start_text)
+ expect(info["shortDescription"].as_s).to start_with(description_start_text)
+
+ # TODO: Update mocks right before the start of PDB podcast, either on friday or saturday (time unknown)
+ # expect(info["descriptionHtml"].as_s).to start_with(
+ # "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - <a href=\"https://aura.com/pbd\">aura.com/pbd</a>"
+ # )
+
+ # Video metadata
+
+ expect(info["genre"].as_s).to eq("Entertainment")
+ expect(info["genreUcid"].as_s?).to be_nil
+ expect(info["license"].as_s).to be_empty
+
+ # Author infos
+
+ expect(info["author"].as_s).to eq("PBD Podcast")
+ expect(info["ucid"].as_s).to eq("UCGX7nGXpz-CmO_Arg-cgJ7A")
+
+ expect(info["authorThumbnail"].as_s).to eq(
+ "https://yt3.ggpht.com/61ArDiQshJrvSXcGLhpFfIO3hlMabe2fksitcf6oGob0Mdr5gztdkXxRljICUodL4iuTSrtxW4A=s48-c-k-c0x00ffffff-no-rj"
+ )
+ expect(info["authorVerified"].as_bool).to be_false
+ expect(info["subCountText"].as_s).to eq("594K")
+ end
+end
diff --git a/spec/locales_spec.cr b/spec/locales_spec.cr
deleted file mode 100644
index 6a083ee7..00000000
--- a/spec/locales_spec.cr
+++ /dev/null
@@ -1,29 +0,0 @@
-require "spec"
-require "json"
-require "../src/invidious/helpers/i18n.cr"
-
-describe "Locales" do
- describe "#consistency" do
- locales_list = LOCALES.keys.select! { |key| key != "en-US" }
-
- locales_list.each do |locale|
- puts "\nChecking locale #{locale}"
- failed = false
-
- # Use "en-US" as the reference
- LOCALES["en-US"].each_key do |ref_key|
- # Catch exception in order to give a hint on what caused
- # the failure, and test one locale completely before failing
- begin
- LOCALES[locale].has_key?(ref_key).should be_true
- rescue
- failed = true
- puts " Missing key in locale #{locale}: '#{ref_key}'"
- end
- end
-
- # Throw failed assertion exception in here
- failed.should be_false
- end
- end
-end
diff --git a/spec/parsers_helper.cr b/spec/parsers_helper.cr
new file mode 100644
index 00000000..6589acad
--- /dev/null
+++ b/spec/parsers_helper.cr
@@ -0,0 +1,35 @@
+require "db"
+require "json"
+require "kemal"
+
+require "protodec/utils"
+
+require "spectator"
+
+require "../src/invidious/exceptions"
+require "../src/invidious/helpers/macros"
+require "../src/invidious/helpers/logger"
+require "../src/invidious/helpers/utils"
+
+require "../src/invidious/videos"
+require "../src/invidious/videos/*"
+require "../src/invidious/comments/content"
+
+require "../src/invidious/helpers/serialized_yt_data"
+require "../src/invidious/yt_backend/extractors"
+require "../src/invidious/yt_backend/extractors_utils"
+
+OUTPUT = File.open(File::NULL, "w")
+LOGGER = Invidious::LogHandler.new(OUTPUT, LogLevel::Off)
+
+def load_mock(file) : Hash(String, JSON::Any)
+ file = File.join(__DIR__, "..", "mocks", file + ".json")
+ content = File.read(file)
+
+ return JSON.parse(content).as_h
+end
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
new file mode 100644
index 00000000..b3060acf
--- /dev/null
+++ b/spec/spec_helper.cr
@@ -0,0 +1,18 @@
+require "kemal"
+require "openssl/hmac"
+require "pg"
+require "protodec/utils"
+require "yaml"
+require "../src/invidious/helpers/*"
+require "../src/invidious/channels/*"
+require "../src/invidious/videos/caption"
+require "../src/invidious/videos"
+require "../src/invidious/playlists"
+require "../src/invidious/search/ctoken"
+require "../src/invidious/trending"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end