using System; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using Paca.App; using Paca.Browser.Profiles; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.E2E; /// /// 20종 추상 아바타 프리셋 및 사용자 커스텀 이미지 업로드 TDD 검증 게이트 /// public sealed class ProfileAvatarPresetAndUploadGateTests { private static string AppAssetsAvatarDir => Path.Combine(SourceTree.Root, "src", "Paca.App", "Assets", "Avatars"); [Fact] public void Gate1_PresetAvatars_20Images_ExistAndValidBitmap_Audit() { // 1. Assets/Avatars 디렉터리가 존재해야 함 Assert.True(Directory.Exists(AppAssetsAvatarDir), $"Avatars directory missing: {AppAssetsAvatarDir}"); // 2. avatar_01.png ~ avatar_20.png 20종 존재 검증 for (int i = 1; i <= 20; i++) { string fileName = $"avatar_{i:D2}.png"; string filePath = Path.Combine(AppAssetsAvatarDir, fileName); Assert.True(File.Exists(filePath), $"Preset avatar missing: {fileName}"); var fileInfo = new FileInfo(filePath); Assert.True(fileInfo.Length >= 1024, $"Avatar file {fileName} is too small ({fileInfo.Length} bytes)"); // 유효한 이미지인지 디코딩 무결성 검증 using var stream = File.OpenRead(filePath); var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); Assert.True(decoder.Frames.Count > 0, $"Avatar file {fileName} has no valid frames"); Assert.True(decoder.Frames[0].PixelWidth > 0 && decoder.Frames[0].PixelHeight > 0); } } [Fact] public void Gate2_ProfileAvatarHelper_GetPresetAvatars_Returns20Items() { var presets = ProfileAvatarHelper.GetPresetAvatarPaths(); Assert.NotNull(presets); Assert.Equal(20, presets.Count); Assert.All(presets, p => Assert.True(File.Exists(p) || p.StartsWith("pack://"))); } [Fact] public void Gate3_ProfileManager_PreservesAvatarImagePath_OnSaveAndReload_Audit() { var tempDir = Path.Combine(Path.GetTempPath(), "paca_test_avatars_" + Guid.NewGuid().ToString("N")); try { Directory.CreateDirectory(tempDir); var manager = new ProfileManager(tempDir); var dummyImagePath = Path.Combine(tempDir, "custom_avatar.png"); File.WriteAllBytes(dummyImagePath, new byte[] { 0x89, 0x50, 0x4E, 0x47 }); // minimal png header var prof = manager.AddProfile("아바타 테스트", "#4F46E5", "\ue19f", dummyImagePath); Assert.Equal(dummyImagePath, prof.AvatarImagePath); // 다시 로드하여 영속화 일치 검증 var reloadedManager = new ProfileManager(tempDir); var reloadedProf = reloadedManager.GetProfile(prof.Id); Assert.NotNull(reloadedProf); Assert.Equal(dummyImagePath, reloadedProf.AvatarImagePath); // 업데이트 시 이미지 변경 및 제거 검증 reloadedManager.UpdateProfile(prof.Id, "이름 변경", "#059669", "\ue0d7", null); var clearedProf = reloadedManager.GetProfile(prof.Id); Assert.NotNull(clearedProf); Assert.Null(clearedProf.AvatarImagePath); } finally { if (Directory.Exists(tempDir)) { try { Directory.Delete(tempDir, true); } catch { } } } } [Fact] public async Task Gate4_ProfileAvatarBadge_RendersImageBrush_WhenImagePathSet_Audit() { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); var success = await sta.Dispatcher.InvokeAsync(() => { // 더미 16x16 PNG 비트맵 생성 var rtb = new RenderTargetBitmap(16, 16, 96, 96, PixelFormats.Pbgra32); var dv = new DrawingVisual(); using (var dc = dv.RenderOpen()) { dc.DrawRectangle(Brushes.Indigo, null, new Rect(0, 0, 16, 16)); } rtb.Render(dv); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); var tempPng = Path.Combine(Path.GetTempPath(), $"paca_avatar_render_{Guid.NewGuid():N}.png"); using (var fs = File.Create(tempPng)) { encoder.Save(fs); } try { var brush = ProfileAvatarHelper.CreateAvatarBrush(tempPng); Assert.NotNull(brush); Assert.Equal(Stretch.UniformToFill, brush.Stretch); var badge = new System.Windows.Controls.Border { Width = 24, Height = 24, CornerRadius = new CornerRadius(12), Background = brush }; badge.Measure(new Size(24, 24)); badge.Arrange(new Rect(0, 0, 24, 24)); badge.UpdateLayout(); var testRtb = new RenderTargetBitmap(24, 24, 96, 96, PixelFormats.Pbgra32); testRtb.Render(badge); var px = new byte[24 * 24 * 4]; testRtb.CopyPixels(px, 24 * 4, 0); // 중앙 픽셀(12, 12)이 Indigo 계열(투명이 아님)인지 확인 int centerIdx = (12 * 24 + 12) * 4; return px[centerIdx + 3] > 0; // Alpha > 0 } finally { try { File.Delete(tempPng); } catch { } } }); Assert.True(success); } [Theory] [InlineData(".png", true)] [InlineData(".jpg", true)] [InlineData(".jpeg", true)] [InlineData(".webp", true)] [InlineData(".bmp", true)] [InlineData(".exe", false)] [InlineData(".bat", false)] [InlineData(".txt", false)] public void Gate5_ProfileAvatarUpload_ValidatesExtension_Audit(string ext, bool expectedValid) { var isValid = ProfileAvatarHelper.IsSupportedImageExtension(ext); Assert.Equal(expectedValid, isValid); } }