Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/SocialMedia/YouTubeUrlTests.cs
T
RichardandClaude Opus 5.5 77062facc9 Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen
Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt).
Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e):
Properties, Record-Parameter, Enum-Member sowie die Parameter der
Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder
anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:18:43 +02:00

140 lines
5.9 KiB
C#

using ClawdDotNet.Tools.SocialMediaManager;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.SocialMedia;
/// <summary>
/// S2 from the inventory: the channel/video statement was interpolated unchecked into an
/// argument string for yt-dlp. UseShellExecute=false prevents
/// shell metacharacters, but not option injection — yt-dlp knows --exec, which executes arbitrary
/// commands.
///
/// The attack cases stay permanently documented here as test cases.
/// </summary>
public sealed class YouTubeUrlTests
{
// ═══════════════════════════════════════════════════════════
// Attacks
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("--exec cmd.exe https://www.youtube.com/@kanal")]
[InlineData("--exec=calc.exe")]
[InlineData("-o /tmp/evil")]
[InlineData("--config-location /tmp/evil.conf")]
[InlineData("--paths /windows/system32")]
[InlineData("-")]
public void Option_like_inputs_are_rejected(string input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData("https://evil.com/video")]
[InlineData("https://youtube.com.attacker.net/@kanal")]
[InlineData("https://notyoutube.com/@kanal")]
[InlineData("http://192.168.178.10:8418/Richard/ClawdDotNet.git")]
[InlineData("file:///C:/Windows/win.ini")]
[InlineData("ftp://example.com/datei")]
public void Foreign_hosts_and_schemes_are_rejected(string url)
{
YouTubeUrl.TryResolveChannelUrl(url, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData("kanal name")] // space
[InlineData("kanal\"name")] // quotation marks
[InlineData("kanal\nname")] // line break
[InlineData("kanal;name")]
[InlineData("kanal&name")]
[InlineData("../../etc/passwd")]
[InlineData("@kanal/../../evil")]
public void Unclean_handles_are_rejected(string input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
error.ShouldNotBeNull();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Empty_inputs_are_rejected(string? input)
{
YouTubeUrl.TryResolveChannelUrl(input, out _, out _).ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Counter-checks — legitimate inputs must keep working
// ═══════════════════════════════════════════════════════════
[Theory]
[InlineData("@Computerphile", "https://www.youtube.com/@Computerphile/videos")]
[InlineData("Computerphile", "https://www.youtube.com/@Computerphile/videos")]
[InlineData("kanal_mit-punkt.name", "https://www.youtube.com/@kanal_mit-punkt.name/videos")]
public void Handles_become_channel_URLs(string input, string expected)
{
YouTubeUrl.TryResolveChannelUrl(input, out var url, out var error).ShouldBeTrue(error);
url.ShouldBe(expected);
}
[Theory]
[InlineData("https://www.youtube.com/watch?v=dQw4w9WgXcQ")]
[InlineData("https://youtube.com/@kanal/videos")]
[InlineData("https://m.youtube.com/watch?v=abc")]
[InlineData("https://music.youtube.com/watch?v=abc")]
[InlineData("https://youtu.be/dQw4w9WgXcQ")]
public void Real_YouTube_URLs_are_let_through(string url)
{
YouTubeUrl.TryResolveChannelUrl(url, out var resolved, out var error).ShouldBeTrue(error);
resolved.ShouldBe(url);
}
// ═══════════════════════════════════════════════════════════
// Building the arguments
// ═══════════════════════════════════════════════════════════
[Fact]
public void The_URL_comes_after_the_end_of_options_separator()
{
// "--" ends the option list. Even if a value looked like an option,
// yt-dlp would treat it as an address after that.
var args = YouTubeUrl.BuildLatestVideoIdArgs("https://www.youtube.com/@kanal/videos");
var separator = args.IndexOf("--");
separator.ShouldBeGreaterThanOrEqualTo(0);
args[^1].ShouldBe("https://www.youtube.com/@kanal/videos");
args.IndexOf(args[^1]).ShouldBeGreaterThan(separator);
}
[Fact]
public void Every_argument_is_its_own_entry()
{
// Crucial: no entry may contain several arguments — otherwise they could
// be split apart again at startup.
var args = YouTubeUrl.BuildAudioDownloadArgs(
@"C:\Pfad mit Leerzeichen\datei.mp3",
"https://www.youtube.com/watch?v=abc",
@"C:\ffmpeg\bin",
convertToMp3: true);
args.ShouldContain(@"C:\Pfad mit Leerzeichen\datei.mp3");
args.ShouldContain(@"C:\ffmpeg\bin");
args.ShouldContain("--audio-format");
args.ShouldContain("mp3");
args[^1].ShouldBe("https://www.youtube.com/watch?v=abc");
}
[Fact]
public void Without_ffmpeg_the_conversion_is_skipped()
{
var args = YouTubeUrl.BuildAudioDownloadArgs(
"/tmp/x.%(ext)s", "https://youtu.be/abc", null, convertToMp3: false);
args.ShouldNotContain("--audio-format");
args.ShouldNotContain("--ffmpeg-location");
}
}