using System.Drawing; using System.Drawing.Imaging; using System.IO; using SharepointToolbox.Core.Models; using SharepointToolbox.Infrastructure.Persistence; using SharepointToolbox.Services; namespace SharepointToolbox.Tests.Services; [Trait("Category", "Unit")] public class BrandingServiceTests : IDisposable { private readonly string _tempRepoFile; private readonly List _tempFiles = new(); public BrandingServiceTests() { _tempRepoFile = Path.GetTempFileName(); File.Delete(_tempRepoFile); } public void Dispose() { if (File.Exists(_tempRepoFile)) File.Delete(_tempRepoFile); if (File.Exists(_tempRepoFile + ".tmp")) File.Delete(_tempRepoFile + ".tmp"); foreach (var f in _tempFiles) { if (File.Exists(f)) File.Delete(f); } } private BrandingRepository CreateRepository() => new(_tempRepoFile); private BrandingService CreateService() => new(CreateRepository()); private string WriteTempFile(byte[] bytes) { var path = Path.GetTempFileName(); File.WriteAllBytes(path, bytes); _tempFiles.Add(path); return path; } // Minimal valid 1x1 PNG bytes private static byte[] MinimalPngBytes() { // Full 1x1 transparent PNG (67 bytes) return new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, // IHDR length 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x01, // width = 1 0x00, 0x00, 0x00, 0x01, // height = 1 0x08, 0x02, // bit depth = 8, color type = RGB 0x00, 0x00, 0x00, // compression, filter, interlace 0x90, 0x77, 0x53, 0xDE, // CRC 0x00, 0x00, 0x00, 0x0C, // IDAT length 0x49, 0x44, 0x41, 0x54, // IDAT 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, // compressed data 0xE2, 0x21, 0xBC, 0x33, // CRC 0x00, 0x00, 0x00, 0x00, // IEND length 0x49, 0x45, 0x4E, 0x44, // IEND 0xAE, 0x42, 0x60, 0x82 // CRC }; } // Minimal valid JPEG bytes (SOI + APP0 JFIF header + EOI) private static byte[] MinimalJpegBytes() { return new byte[] { 0xFF, 0xD8, // SOI 0xFF, 0xE0, // APP0 marker 0x00, 0x10, // length = 16 0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0" 0x01, 0x01, // version 1.1 0x00, // aspect ratio units = 0 0x00, 0x01, 0x00, 0x01, // X/Y density = 1 0x00, 0x00, // thumbnail size 0xFF, 0xD9 // EOI }; } [Fact] public async Task ImportLogoAsync_ValidPng_ReturnsPngLogoData() { var service = CreateService(); var pngBytes = MinimalPngBytes(); var path = WriteTempFile(pngBytes); var result = await service.ImportLogoAsync(path); Assert.Equal("image/png", result.MimeType); Assert.Equal(Convert.ToBase64String(pngBytes), result.Base64); } [Fact] public async Task ImportLogoAsync_ValidJpeg_ReturnsJpegLogoData() { var service = CreateService(); var jpegBytes = MinimalJpegBytes(); var path = WriteTempFile(jpegBytes); var result = await service.ImportLogoAsync(path); Assert.Equal("image/jpeg", result.MimeType); } [Fact] public async Task ImportLogoAsync_BmpFile_ThrowsInvalidDataExceptionMentioningPngAndJpg() { var service = CreateService(); // BMP magic bytes: 0x42 0x4D var bmpBytes = new byte[] { 0x42, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; var path = WriteTempFile(bmpBytes); var ex = await Assert.ThrowsAsync(() => service.ImportLogoAsync(path)); Assert.Contains("PNG", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Contains("JPG", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ImportLogoAsync_EmptyFile_ThrowsInvalidDataException() { var service = CreateService(); var path = WriteTempFile(Array.Empty()); await Assert.ThrowsAsync(() => service.ImportLogoAsync(path)); } [Fact] public async Task ImportLogoAsync_FileUnder512KB_ReturnOriginalBytesUnmodified() { var service = CreateService(); var pngBytes = MinimalPngBytes(); var path = WriteTempFile(pngBytes); var result = await service.ImportLogoAsync(path); Assert.Equal(Convert.ToBase64String(pngBytes), result.Base64); } [Fact] public async Task ImportLogoAsync_FileOver512KB_ReturnsCompressedUnder512KB() { var service = CreateService(); // Create a large PNG image in memory (400x400 random pixels) var largePngPath = Path.GetTempFileName(); _tempFiles.Add(largePngPath); using (var bmp = new Bitmap(400, 400)) { var rng = new Random(42); for (int y = 0; y < 400; y++) for (int x = 0; x < 400; x++) bmp.SetPixel(x, y, Color.FromArgb(255, rng.Next(256), rng.Next(256), rng.Next(256))); bmp.Save(largePngPath, ImageFormat.Png); } var fileSize = new FileInfo(largePngPath).Length; // PNG with random pixels should exceed 512 KB // If not, we'll pad it if (fileSize <= 512 * 1024) { // Generate a bigger image to be sure using var bmp = new Bitmap(800, 800); var rng = new Random(42); for (int y = 0; y < 800; y++) for (int x = 0; x < 800; x++) bmp.SetPixel(x, y, Color.FromArgb(255, rng.Next(256), rng.Next(256), rng.Next(256))); bmp.Save(largePngPath, ImageFormat.Png); } fileSize = new FileInfo(largePngPath).Length; Assert.True(fileSize > 512 * 1024, $"Test setup: PNG file must be > 512 KB but was {fileSize} bytes"); var result = await service.ImportLogoAsync(largePngPath); var decodedBytes = Convert.FromBase64String(result.Base64); Assert.True(decodedBytes.Length <= 512 * 1024, $"Compressed result must be <= 512 KB but was {decodedBytes.Length} bytes"); } [Fact] public async Task SaveMspLogoAsync_PersistsLogoInRepository() { var repo = CreateRepository(); var service = new BrandingService(repo); var logo = new LogoData { Base64 = "abc123==", MimeType = "image/png" }; await service.SaveMspLogoAsync(logo); var loaded = await repo.LoadAsync(); Assert.NotNull(loaded.MspLogo); Assert.Equal("abc123==", loaded.MspLogo.Base64); Assert.Equal("image/png", loaded.MspLogo.MimeType); } [Fact] public async Task ClearMspLogoAsync_SetsMspLogoToNull() { var repo = CreateRepository(); var service = new BrandingService(repo); var logo = new LogoData { Base64 = "abc123==", MimeType = "image/png" }; await service.SaveMspLogoAsync(logo); await service.ClearMspLogoAsync(); var loaded = await repo.LoadAsync(); Assert.Null(loaded.MspLogo); } [Fact] public async Task GetMspLogoAsync_WhenNoLogoConfigured_ReturnsNull() { var service = CreateService(); var result = await service.GetMspLogoAsync(); Assert.Null(result); } [Fact] public async Task ImportLogoFromBytesAsync_ValidPngBytes_ReturnsPngLogoData() { var service = CreateService(); var pngBytes = MinimalPngBytes(); var result = await service.ImportLogoFromBytesAsync(pngBytes); Assert.Equal("image/png", result.MimeType); Assert.Equal(Convert.ToBase64String(pngBytes), result.Base64); } [Fact] public async Task ImportLogoFromBytesAsync_InvalidBytes_ThrowsInvalidDataException() { var service = CreateService(); var invalidBytes = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }; await Assert.ThrowsAsync(() => service.ImportLogoFromBytesAsync(invalidBytes)); } }