T3-11/14/15 + M2 잔여: 권한 정책 UI(PermissionGate/PermissionRequested 다이얼로그+SavesInProfile), 추적 방지(TrackerBlocklist/WebResourceRequested 차단+실드 배지·팝업), 주소창 진행 바+파라미터 제거 복사(UrlCleaner/NavProgressTracker), U5 완료 알림(Mobile.Core 감지기+Plugin.LocalNotification), 재생중 ongoing 미디어 알림, 썸네일/해상도 API(Thumbnailer 사이드카 캐시+/thumb)+라이브러리 그리드, 외부망 안내(ConnectionTroubleshoot). 147 tests GREEN

This commit is contained in:
Yun Chan 2026-08-15 18:11:49 +09:00
parent 27c91c04dd
commit 46c6c321f2
26 changed files with 1058 additions and 30 deletions

View file

@ -155,6 +155,44 @@ public class RemoteServerE2ETests : IDisposable
Assert.Equal(93.5, persisted!.PositionSec);
}
[Fact]
public async Task 썸네일_API_E2E_생성_캐시_404()
{
Directory.CreateDirectory(_libDir);
var mediaPath = Path.Combine(_libDir, "clip.mp4");
File.WriteAllBytes(mediaPath, new byte[2048]);
// 가짜 ffmpeg: 출력 경로에 JPEG 생성
var fake = new Tests.Platform.FakeProcessRunner
{
Behavior = (spec, _, _) =>
{
File.WriteAllBytes(spec.Args[^1], new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0x01 });
return Task.FromResult(new VideoDownloader.Core.Platform.ProcessResult(0, "", ""));
},
};
await using var server = await ServerHost.StartAsync(new ServerOptions
{
Port = 0, Token = "thumb-token", LibraryFolder = _libDir,
Queue = new FakeQueue(),
Thumbnailer = new Thumbnailer(fake),
});
using var http = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{server.Port}") };
// 1) 익명 401
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, (await http.GetAsync("/api/library/xyz/thumb")).StatusCode);
// 2) 없는 항목 404
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "thumb-token");
Assert.Equal(System.Net.HttpStatusCode.NotFound, (await http.GetAsync("/api/library/xyz/thumb")).StatusCode);
// 3) 실제 항목 → 200 + JPEG 매직
var lib = (await http.GetFromJsonAsync<List<LibraryItemDto>>("/api/library"))!;
var thumb = await http.GetByteArrayAsync($"/api/library/{lib[0].Id}/thumb");
Assert.True(thumb.Length >= 2 && thumb[0] == 0xFF && thumb[1] == 0xD8, "JPEG 매직 바이트");
}
public void Dispose()
{
try { Directory.Delete(_libDir, true); } catch { }

View file

@ -0,0 +1,84 @@
using VideoDownloader.Core.Platform;
using VideoDownloader.Server;
using VideoDownloader.Tests.Platform;
namespace VideoDownloader.Tests.Server;
/// <summary>M2 라이브러리 썸네일/해상도 — ffmpeg 서브프로세스를 Fake 러너로 검증(사이드카 캐시).</summary>
public class ThumbnailerTests
{
private string _dir = "";
private string NewMedia()
{
_dir = Path.Combine(Path.GetTempPath(), $"vd_thumb_{Guid.NewGuid():N}");
Directory.CreateDirectory(_dir);
var p = Path.Combine(_dir, "clip.mp4");
File.WriteAllBytes(p, new byte[100]);
return p;
}
private static Task<ProcessResult> FakeFfmpegCreatesJpg(ProcessSpec spec, Action<string, string>? _, CancellationToken __)
{
// 출력 경로(마지막 인자)에 가짜 JPEG 생성
var outPath = spec.Args[^1];
File.WriteAllBytes(outPath, new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }); // JPEG SOI/APP0
return Task.FromResult(new ProcessResult(0, "", ""));
}
private static Task<ProcessResult> FakeFfmpegReportsResolution(ProcessSpec spec, Action<string, string>? onLine, CancellationToken _)
{
var line = " Stream #0:0[0x1](und): Video: h264 (High), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 5000 kb/s";
onLine?.Invoke("err", line);
return Task.FromResult(new ProcessResult(0, "", line));
}
[Fact]
public async Task 썸네일_생성후_사이드카_캐시로_재실행_없음()
{
var media = NewMedia();
var runner = new FakeProcessRunner { Behavior = FakeFfmpegCreatesJpg };
var t = new Thumbnailer(runner);
var bytes1 = await t.GetThumbnailAsync("ffmpeg", media);
Assert.NotNull(bytes1);
Assert.True(bytes1!.Length == 4);
runner.Behavior = (_, _, _) => throw new Xunit.Sdk.XunitException("캐시 히트 시 ffmpeg 재실행 없음");
var bytes2 = await t.GetThumbnailAsync("ffmpeg", media); // 캐시
Assert.Equal(bytes1, bytes2);
Directory.Delete(_dir, true);
}
[Fact]
public async Task ffmpeg_실패_시_null()
{
var media = NewMedia();
var runner = new FakeProcessRunner { Behavior = (_, _, _) => Task.FromResult(new ProcessResult(1, "", "boom")) };
Assert.Null(await new Thumbnailer(runner).GetThumbnailAsync("ffmpeg", media));
Directory.Delete(_dir, true);
}
[Fact]
public async Task 해상도_추출후_사이드카_캐시()
{
var media = NewMedia();
var runner = new FakeProcessRunner { Behavior = FakeFfmpegReportsResolution };
var t = new Thumbnailer(runner);
Assert.Equal("1920x1080", await t.GetResolutionAsync("ffmpeg", media));
runner.Behavior = (_, _, _) => throw new Xunit.Sdk.XunitException("캐시 히트 시 재실행 없음");
Assert.Equal("1920x1080", await t.GetResolutionAsync("ffmpeg", media));
Directory.Delete(_dir, true);
}
[Fact]
public async Task 해상기_추출_불가_시_null()
{
var media = NewMedia();
var runner = new FakeProcessRunner { Behavior = (_, _, _) => Task.FromResult(new ProcessResult(0, "", "no streams")) };
Assert.Null(await new Thumbnailer(runner).GetResolutionAsync("ffmpeg", media));
Directory.Delete(_dir, true);
}
}