-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathtest_ytdlp_utils.py
More file actions
461 lines (392 loc) · 16.6 KB
/
test_ytdlp_utils.py
File metadata and controls
461 lines (392 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
"""
Unit tests for yt-dlp utility modules: cookie_validator and config_builder.
Covers:
- cookie_validator: domain checks, line parsing, file validation, summaries
- config_builder: info/download opts, cookie availability, path resolution
All console output must be in English only (no emoji, no Chinese).
"""
import time
import pytest
from unittest.mock import patch, MagicMock
from video_transcript_api.utils.ytdlp.cookie_validator import (
_is_youtube_domain,
_parse_netscape_cookie_line,
validate_youtube_cookie_file,
get_validation_summary,
CookieValidationResult,
)
from video_transcript_api.utils.ytdlp.config_builder import (
YtdlpConfigBuilder,
DEFAULT_SOCKET_TIMEOUT,
DEFAULT_RETRIES,
DEFAULT_FRAGMENT_RETRIES,
DEFAULT_EXTRACTOR_RETRIES,
DEFAULT_PLAYER_CLIENTS,
)
# ===========================================================================
# cookie_validator tests
# ===========================================================================
class TestIsYoutubeDomain:
"""Test _is_youtube_domain helper."""
@pytest.mark.parametrize("domain", [
".youtube.com",
"youtube.com",
"www.youtube.com",
".google.com",
])
def test_youtube_domains_return_true(self, domain):
assert _is_youtube_domain(domain) is True
@pytest.mark.parametrize("domain", [
"example.com",
"notyoutube.org",
".bing.com",
"youtube.org",
"",
])
def test_non_youtube_domains_return_false(self, domain):
assert _is_youtube_domain(domain) is False
def test_domain_is_case_insensitive(self):
assert _is_youtube_domain("YOUTUBE.COM") is True
assert _is_youtube_domain("YouTube.Com") is True
def test_domain_with_whitespace(self):
assert _is_youtube_domain(" .youtube.com ") is True
class TestParseNetscapeCookieLine:
"""Test _parse_netscape_cookie_line parser."""
def test_valid_7_field_line(self):
line = ".youtube.com\tTRUE\t/\tTRUE\t1700000000\tSID\tabc123"
result = _parse_netscape_cookie_line(line)
assert result is not None
assert result["domain"] == ".youtube.com"
assert result["flag"] == "TRUE"
assert result["path"] == "/"
assert result["secure"] is True
assert result["expiration"] == 1700000000
assert result["name"] == "SID"
assert result["value"] == "abc123"
def test_comment_line_returns_none(self):
assert _parse_netscape_cookie_line("# Netscape HTTP Cookie File") is None
def test_empty_line_returns_none(self):
assert _parse_netscape_cookie_line("") is None
assert _parse_netscape_cookie_line(" ") is None
def test_invalid_field_count_returns_none(self):
assert _parse_netscape_cookie_line("only\ttwo") is None
assert _parse_netscape_cookie_line("a\tb\tc\td\te\tf") is None # 6 fields
def test_non_digit_expiration_defaults_to_zero(self):
line = ".youtube.com\tTRUE\t/\tFALSE\tnotanumber\tNAME\tVALUE"
result = _parse_netscape_cookie_line(line)
assert result is not None
assert result["expiration"] == 0
def test_secure_false(self):
line = ".youtube.com\tTRUE\t/\tFALSE\t0\tNAME\tVALUE"
result = _parse_netscape_cookie_line(line)
assert result is not None
assert result["secure"] is False
class TestValidateYoutubeCookieFile:
"""Test validate_youtube_cookie_file with real temp files."""
def test_file_not_found(self, tmp_path):
result = validate_youtube_cookie_file(str(tmp_path / "nonexistent.txt"))
assert result.is_valid is False
assert result.file_exists is False
assert "does not exist" in result.error
def test_path_is_directory(self, tmp_path):
result = validate_youtube_cookie_file(str(tmp_path))
assert result.is_valid is False
assert result.file_exists is True
assert "not a file" in result.error
def test_empty_file(self, tmp_path):
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text("")
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is False
assert "empty" in result.error
def test_comments_only_file(self, tmp_path):
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text("# Netscape HTTP Cookie File\n# comment\n")
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is False
assert "empty" in result.error
def test_no_youtube_cookies(self, tmp_path):
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
".example.com\tTRUE\t/\tFALSE\t0\tSOME_COOKIE\tvalue\n"
)
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is False
assert "No YouTube cookies" in result.error
def test_valid_file_with_auth_cookies(self, tmp_path):
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
lines = [
"# Netscape HTTP Cookie File",
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tSID\tabc123",
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tLOGIN_INFO\txyz789",
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tHSID\tdef456",
]
cookie_file.write_text("\n".join(lines) + "\n")
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is True
assert result.file_exists is True
assert result.format_valid is True
assert result.youtube_cookie_count == 3
assert result.has_auth_cookies is True
assert "SID" in result.auth_cookies_found
assert "LOGIN_INFO" in result.auth_cookies_found
assert "HSID" in result.auth_cookies_found
assert result.expired_count == 0
def test_expired_cookies_generate_warning(self, tmp_path):
past_ts = str(int(time.time()) - 86400)
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
lines = [
f".youtube.com\tTRUE\t/\tTRUE\t{past_ts}\tSID\texpired_val",
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tLOGIN_INFO\tvalid_val",
]
cookie_file.write_text("\n".join(lines) + "\n")
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is True
assert result.expired_count == 1
assert any("expired" in w for w in result.warnings)
def test_session_cookies_are_not_expired(self, tmp_path):
"""Cookies with expiration 0 are session cookies and should be valid."""
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
".youtube.com\tTRUE\t/\tTRUE\t0\tSID\tsession_val\n"
)
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is True
assert result.expired_count == 0
assert "SID" in result.auth_cookies_found
def test_invalid_format_lines_only(self, tmp_path):
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text("this is not a cookie\nanother bad line\n")
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is False
assert "No valid Netscape format" in result.error
def test_no_auth_cookies_warning(self, tmp_path):
"""YouTube cookies without auth names should produce a warning."""
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tPREF\tsome_pref\n"
)
result = validate_youtube_cookie_file(str(cookie_file))
assert result.is_valid is True
assert result.has_auth_cookies is False
assert any("No authentication cookies" in w for w in result.warnings)
class TestGetValidationSummary:
"""Test get_validation_summary output formatting."""
def test_valid_result_with_auth(self):
result = CookieValidationResult(
is_valid=True,
youtube_cookie_count=3,
has_auth_cookies=True,
auth_cookies_found={"SID", "LOGIN_INFO"},
expired_count=0,
)
summary = get_validation_summary(result)
assert "PASSED" in summary
assert "YouTube cookies: 3" in summary
assert "SID" in summary
assert "LOGIN_INFO" in summary
def test_valid_result_without_auth(self):
result = CookieValidationResult(
is_valid=True,
youtube_cookie_count=2,
has_auth_cookies=False,
expired_count=1,
warnings=["1 cookie(s) have expired"],
)
summary = get_validation_summary(result)
assert "PASSED" in summary
assert "Auth cookies: NONE" in summary
assert "Warning:" in summary
def test_invalid_result(self):
result = CookieValidationResult(
is_valid=False,
error="Cookie file does not exist: /fake/path",
)
summary = get_validation_summary(result)
assert "FAILED" in summary
assert "does not exist" in summary
# ===========================================================================
# config_builder tests
# ===========================================================================
class TestYtdlpConfigBuilderDefaults:
"""Test builder with empty / minimal config."""
def test_defaults_when_config_empty(self):
builder = YtdlpConfigBuilder({})
assert builder.ytdlp_config == {}
def test_is_cookie_available_false_when_not_enabled(self):
builder = YtdlpConfigBuilder({})
assert builder.is_cookie_available() is False
def test_should_fallback_defaults_true(self):
builder = YtdlpConfigBuilder({})
assert builder.should_fallback() is True
class TestBuildInfoOpts:
"""Test build_info_opts returns correct options."""
def test_info_opts_has_skip_download(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_info_opts()
assert opts["skip_download"] is True
assert opts["extract_flat"] is False
def test_info_opts_uses_default_timeouts(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_info_opts()
assert opts["socket_timeout"] == DEFAULT_SOCKET_TIMEOUT
assert opts["retries"] == DEFAULT_RETRIES
assert opts["fragment_retries"] == DEFAULT_FRAGMENT_RETRIES
assert opts["extractor_retries"] == DEFAULT_EXTRACTOR_RETRIES
def test_info_opts_custom_timeout(self):
config = {"ytdlp": {"socket_timeout": 60}}
builder = YtdlpConfigBuilder(config)
opts = builder.build_info_opts()
assert opts["socket_timeout"] == 60
def test_info_opts_no_cookie_by_default(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_info_opts()
assert "cookiefile" not in opts
def test_info_opts_includes_player_clients(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_info_opts()
assert opts["extractor_args"]["youtube"]["player_client"] == DEFAULT_PLAYER_CLIENTS
def test_info_opts_custom_player_clients(self):
config = {"ytdlp": {"player_client": ["web", "android"]}}
builder = YtdlpConfigBuilder(config)
opts = builder.build_info_opts()
assert opts["extractor_args"]["youtube"]["player_client"] == ["web", "android"]
class TestBuildDownloadOpts:
"""Test build_download_opts returns correct options."""
def test_download_opts_audio_only_has_postprocessors(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_download_opts("/tmp/test.%(ext)s", audio_only=True)
assert opts["outtmpl"] == "/tmp/test.%(ext)s"
assert len(opts["postprocessors"]) == 1
pp = opts["postprocessors"][0]
assert pp["key"] == "FFmpegExtractAudio"
assert pp["preferredcodec"] == "mp3"
def test_download_opts_not_audio_only(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_download_opts("/tmp/test.%(ext)s", audio_only=False)
assert opts["format"] == "best"
assert "postprocessors" not in opts
def test_download_opts_has_hls_option(self):
builder = YtdlpConfigBuilder({})
opts = builder.build_download_opts("/tmp/out.%(ext)s")
assert opts["hls_prefer_native"] is True
assert opts["skip_unavailable_fragments"] is False
class TestCookieAvailability:
"""Test is_cookie_available and cookie path resolution."""
def test_cookie_available_when_valid(self, tmp_path):
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tSID\tval\n"
)
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": str(cookie_file),
}
}
}
builder = YtdlpConfigBuilder(config)
assert builder.is_cookie_available() is True
def test_cookie_not_available_when_file_missing(self):
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": "/nonexistent/cookies.txt",
}
}
}
builder = YtdlpConfigBuilder(config)
assert builder.is_cookie_available() is False
def test_cookie_not_available_when_disabled(self):
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": False,
"file_path": "/some/cookies.txt",
}
}
}
builder = YtdlpConfigBuilder(config)
assert builder.is_cookie_available() is False
def test_cookie_path_resolution_relative(self, tmp_path):
"""Relative paths should be resolved to absolute."""
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tSID\tval\n"
)
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": str(cookie_file),
}
}
}
builder = YtdlpConfigBuilder(config)
path = builder.get_cookie_file_path()
assert path is not None
# Already absolute in test, just verify it stays absolute
assert path.startswith("/")
def test_info_opts_includes_cookie_when_available(self, tmp_path):
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tSID\tval\n"
)
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": str(cookie_file),
}
}
}
builder = YtdlpConfigBuilder(config)
opts = builder.build_info_opts(use_cookie=True)
assert "cookiefile" in opts
def test_info_opts_excludes_cookie_when_use_cookie_false(self, tmp_path):
future_ts = str(int(time.time()) + 86400)
cookie_file = tmp_path / "cookies.txt"
cookie_file.write_text(
f".youtube.com\tTRUE\t/\tTRUE\t{future_ts}\tSID\tval\n"
)
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": str(cookie_file),
}
}
}
builder = YtdlpConfigBuilder(config)
opts = builder.build_info_opts(use_cookie=False)
assert "cookiefile" not in opts
class TestConfigSummary:
"""Test get_config_summary formatting."""
def test_summary_without_cookie(self):
builder = YtdlpConfigBuilder({})
summary = builder.get_config_summary()
assert "Socket timeout" in summary
assert "Cookie enabled: False" in summary
def test_summary_with_cookie_enabled(self, tmp_path):
config = {
"ytdlp": {
"youtube_cookie": {
"enabled": True,
"file_path": "/some/path.txt",
"fallback_without_cookie": True,
}
}
}
builder = YtdlpConfigBuilder(config)
summary = builder.get_config_summary()
assert "Cookie enabled: True" in summary
assert "Cookie file: /some/path.txt" in summary
assert "Fallback mode: True" in summary