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
This commit is contained in:
@@ -220,4 +220,25 @@ public class BrandingServiceTests : IDisposable
|
|||||||
|
|
||||||
Assert.Null(result);
|
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<InvalidDataException>(() => service.ImportLogoFromBytesAsync(invalidBytes));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,32 @@ public class ProfileServiceTests : IDisposable
|
|||||||
await Assert.ThrowsAsync<KeyNotFoundException>(() => service.DeleteProfileAsync("NonExistent"));
|
await Assert.ThrowsAsync<KeyNotFoundException>(() => 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<KeyNotFoundException>(() => service.UpdateProfileAsync(profile));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SaveAsync_JsonOutput_UsesProfilesRootKey()
|
public async Task SaveAsync_JsonOutput_UsesProfilesRootKey()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<!-- Suppress NU1701: LiveCharts2 transitive deps lack net10.0 targets but work at runtime -->
|
||||||
|
<NoWarn>$(NoWarn);NU1701</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -30,7 +30,15 @@ public class BrandingService : IBrandingService
|
|||||||
public async Task<LogoData> ImportLogoAsync(string filePath)
|
public async Task<LogoData> ImportLogoAsync(string filePath)
|
||||||
{
|
{
|
||||||
var bytes = await File.ReadAllBytesAsync(filePath);
|
var bytes = await File.ReadAllBytesAsync(filePath);
|
||||||
|
return await ImportLogoFromBytesAsync(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
public Task<LogoData> ImportLogoFromBytesAsync(byte[] bytes)
|
||||||
|
{
|
||||||
var mimeType = DetectMimeType(bytes);
|
var mimeType = DetectMimeType(bytes);
|
||||||
|
|
||||||
if (bytes.Length > MaxSizeBytes)
|
if (bytes.Length > MaxSizeBytes)
|
||||||
@@ -38,11 +46,11 @@ public class BrandingService : IBrandingService
|
|||||||
bytes = CompressToLimit(bytes, mimeType, MaxSizeBytes);
|
bytes = CompressToLimit(bytes, mimeType, MaxSizeBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LogoData
|
return Task.FromResult(new LogoData
|
||||||
{
|
{
|
||||||
Base64 = Convert.ToBase64String(bytes),
|
Base64 = Convert.ToBase64String(bytes),
|
||||||
MimeType = mimeType
|
MimeType = mimeType
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveMspLogoAsync(LogoData logo)
|
public async Task SaveMspLogoAsync(LogoData logo)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace SharepointToolbox.Services;
|
|||||||
public interface IBrandingService
|
public interface IBrandingService
|
||||||
{
|
{
|
||||||
Task<LogoData> ImportLogoAsync(string filePath);
|
Task<LogoData> ImportLogoAsync(string filePath);
|
||||||
|
Task<LogoData> ImportLogoFromBytesAsync(byte[] bytes);
|
||||||
Task SaveMspLogoAsync(LogoData logo);
|
Task SaveMspLogoAsync(LogoData logo);
|
||||||
Task ClearMspLogoAsync();
|
Task ClearMspLogoAsync();
|
||||||
Task<LogoData?> GetMspLogoAsync();
|
Task<LogoData?> GetMspLogoAsync();
|
||||||
|
|||||||
@@ -51,4 +51,13 @@ public class ProfileService
|
|||||||
profiles.Remove(target);
|
profiles.Remove(target);
|
||||||
await _repository.SaveAsync(profiles);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user