Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
5.9 KiB
C#
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 Optionsartige_Eingaben_werden_abgelehnt(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 Fremde_Hosts_und_Schemata_werden_abgelehnt(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 Unsaubere_Handles_werden_abgelehnt(string input)
|
|
{
|
|
YouTubeUrl.TryResolveChannelUrl(input, out _, out var error).ShouldBeFalse();
|
|
error.ShouldNotBeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Leere_Eingaben_werden_abgelehnt(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_werden_zu_Kanal_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 Echte_YouTube_URLs_werden_durchgelassen(string url)
|
|
{
|
|
YouTubeUrl.TryResolveChannelUrl(url, out var resolved, out var error).ShouldBeTrue(error);
|
|
resolved.ShouldBe(url);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Building the arguments
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
[Fact]
|
|
public void Die_URL_steht_hinter_dem_Optionsende_Trenner()
|
|
{
|
|
// "--" 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 Jedes_Argument_ist_ein_eigener_Eintrag()
|
|
{
|
|
// 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 Ohne_ffmpeg_entfaellt_die_Konvertierung()
|
|
{
|
|
var args = YouTubeUrl.BuildAudioDownloadArgs(
|
|
"/tmp/x.%(ext)s", "https://youtu.be/abc", null, convertToMp3: false);
|
|
|
|
args.ShouldNotContain("--audio-format");
|
|
args.ShouldNotContain("--ffmpeg-location");
|
|
}
|
|
}
|