From 9e850b07f278cb2d9f2ddc9e1ba1477dd75667b9 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 8 Apr 2026 14:34:11 +0200 Subject: [PATCH] feat(11-04): add UpdateProfileAsync to ProfileService and ImportLogoFromBytesAsync to BrandingService - ProfileService.UpdateProfileAsync: replaces profile by name and persists the change - IBrandingService: add ImportLogoFromBytesAsync to interface contract - BrandingService.ImportLogoFromBytesAsync: validates magic bytes, compresses if > 512KB, returns LogoData - BrandingService.ImportLogoAsync: refactored to delegate to ImportLogoFromBytesAsync - ProfileServiceTests: 2 new tests (UpdateProfileAsync happy path + KeyNotFoundException) - BrandingServiceTests: 2 new tests (ImportLogoFromBytesAsync valid PNG + invalid bytes) - Tests.csproj: suppress NU1701 for pre-existing LiveCharts2/OpenTK transitive warnings --- .../Services/BrandingServiceTests.cs | 21 +++++++++++++++ .../Services/ProfileServiceTests.cs | 26 +++++++++++++++++++ .../SharepointToolbox.Tests.csproj | 2 ++ SharepointToolbox/Services/BrandingService.cs | 12 +++++++-- .../Services/IBrandingService.cs | 1 + SharepointToolbox/Services/ProfileService.cs | 9 +++++++ 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/SharepointToolbox.Tests/Services/BrandingServiceTests.cs b/SharepointToolbox.Tests/Services/BrandingServiceTests.cs index e0f1eb9..91607e4 100644 --- a/SharepointToolbox.Tests/Services/BrandingServiceTests.cs +++ b/SharepointToolbox.Tests/Services/BrandingServiceTests.cs @@ -220,4 +220,25 @@ public class BrandingServiceTests : IDisposable 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)); + } } diff --git a/SharepointToolbox.Tests/Services/ProfileServiceTests.cs b/SharepointToolbox.Tests/Services/ProfileServiceTests.cs index c66aac9..834b5bf 100644 --- a/SharepointToolbox.Tests/Services/ProfileServiceTests.cs +++ b/SharepointToolbox.Tests/Services/ProfileServiceTests.cs @@ -147,6 +147,32 @@ public class ProfileServiceTests : IDisposable await Assert.ThrowsAsync(() => service.DeleteProfileAsync("NonExistent")); } + [Fact] + public async Task UpdateProfileAsync_UpdatesExistingProfile_AndPersists() + { + var service = CreateService(); + var profile = new TenantProfile { Name = "UpdateMe", TenantUrl = "https://update.sharepoint.com", ClientId = "cid-update" }; + await service.AddProfileAsync(profile); + + // Mutate — set a ClientLogo to simulate logo update + profile.ClientLogo = new SharepointToolbox.Core.Models.LogoData { Base64 = "abc==", MimeType = "image/png" }; + await service.UpdateProfileAsync(profile); + + var profiles = await service.GetProfilesAsync(); + Assert.Single(profiles); + Assert.NotNull(profiles[0].ClientLogo); + Assert.Equal("abc==", profiles[0].ClientLogo!.Base64); + } + + [Fact] + public async Task UpdateProfileAsync_ProfileNotFound_ThrowsKeyNotFoundException() + { + var service = CreateService(); + var profile = new TenantProfile { Name = "NonExistent", TenantUrl = "https://x.sharepoint.com", ClientId = "cid" }; + + await Assert.ThrowsAsync(() => service.UpdateProfileAsync(profile)); + } + [Fact] public async Task SaveAsync_JsonOutput_UsesProfilesRootKey() { diff --git a/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj b/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj index d870a37..efe2bfa 100644 --- a/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj +++ b/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj @@ -6,6 +6,8 @@ enable enable false + + $(NoWarn);NU1701 diff --git a/SharepointToolbox/Services/BrandingService.cs b/SharepointToolbox/Services/BrandingService.cs index 11abc5d..ea0feb3 100644 --- a/SharepointToolbox/Services/BrandingService.cs +++ b/SharepointToolbox/Services/BrandingService.cs @@ -30,7 +30,15 @@ public class BrandingService : IBrandingService public async Task ImportLogoAsync(string filePath) { var bytes = await File.ReadAllBytesAsync(filePath); + return await ImportLogoFromBytesAsync(bytes); + } + /// + /// Validates raw bytes as PNG or JPEG via magic bytes, auto-compresses if over 512 KB, + /// and returns a LogoData record. Used when bytes are obtained from a stream (e.g. Entra branding API). + /// + public Task ImportLogoFromBytesAsync(byte[] bytes) + { var mimeType = DetectMimeType(bytes); if (bytes.Length > MaxSizeBytes) @@ -38,11 +46,11 @@ public class BrandingService : IBrandingService bytes = CompressToLimit(bytes, mimeType, MaxSizeBytes); } - return new LogoData + return Task.FromResult(new LogoData { Base64 = Convert.ToBase64String(bytes), MimeType = mimeType - }; + }); } public async Task SaveMspLogoAsync(LogoData logo) diff --git a/SharepointToolbox/Services/IBrandingService.cs b/SharepointToolbox/Services/IBrandingService.cs index 049abb3..89519f8 100644 --- a/SharepointToolbox/Services/IBrandingService.cs +++ b/SharepointToolbox/Services/IBrandingService.cs @@ -5,6 +5,7 @@ namespace SharepointToolbox.Services; public interface IBrandingService { Task ImportLogoAsync(string filePath); + Task ImportLogoFromBytesAsync(byte[] bytes); Task SaveMspLogoAsync(LogoData logo); Task ClearMspLogoAsync(); Task GetMspLogoAsync(); diff --git a/SharepointToolbox/Services/ProfileService.cs b/SharepointToolbox/Services/ProfileService.cs index 1d3bbe0..5cf5b32 100644 --- a/SharepointToolbox/Services/ProfileService.cs +++ b/SharepointToolbox/Services/ProfileService.cs @@ -51,4 +51,13 @@ public class ProfileService profiles.Remove(target); await _repository.SaveAsync(profiles); } + + public async Task UpdateProfileAsync(TenantProfile profile) + { + var profiles = (await _repository.LoadAsync()).ToList(); + var idx = profiles.FindIndex(p => p.Name == profile.Name); + if (idx < 0) throw new KeyNotFoundException($"Profile '{profile.Name}' not found."); + profiles[idx] = profile; + await _repository.SaveAsync(profiles); + } }