From 809ac8613bd9bd3b13a5f95d080e242516a2f0c6 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 9 Apr 2026 15:19:37 +0200 Subject: [PATCH] feat(19-02): add app registration UI to profile dialog and 7 ViewModel tests - ProfileManagementDialog.xaml: height 750, new Row 4 with Register/Remove buttons - BooleanToVisibilityConverter added to Window.Resources - Fallback instructions panel bound to ShowFallbackInstructions - RegistrationStatus text block with StringToVisibilityConverter - Buttons row shifted to Row 5 - ProfileManagementViewModelRegistrationTests: 7 unit tests, all passing - ProfileManagementViewModelLogoTests: updated to 5-param constructor --- .../ProfileManagementViewModelLogoTests.cs | 11 +- ...ileManagementViewModelRegistrationTests.cs | 157 ++++++++++++++++++ .../Dialogs/ProfileManagementDialog.xaml | 47 +++++- 3 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs diff --git a/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs b/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs index ee49ccf..192457b 100644 --- a/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs +++ b/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs @@ -15,6 +15,7 @@ public class ProfileManagementViewModelLogoTests : IDisposable { private readonly string _tempFile; private readonly Mock _mockBranding; + private readonly Mock _mockAppReg; private readonly GraphClientFactory _graphClientFactory; private readonly ILogger _logger; @@ -23,6 +24,7 @@ public class ProfileManagementViewModelLogoTests : IDisposable _tempFile = Path.GetTempFileName(); File.Delete(_tempFile); _mockBranding = new Mock(); + _mockAppReg = new Mock(); _graphClientFactory = new GraphClientFactory(new MsalClientFactory()); _logger = NullLogger.Instance; } @@ -40,7 +42,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable profileService, _mockBranding.Object, _graphClientFactory, - _logger); + _logger, + _mockAppReg.Object); } [Fact] @@ -102,7 +105,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable profileService, _mockBranding.Object, _graphClientFactory, - _logger); + _logger, + _mockAppReg.Object); vm.SelectedProfile = profile; @@ -173,7 +177,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable profileService, _mockBranding.Object, _graphClientFactory, - _logger); + _logger, + _mockAppReg.Object); vm.SelectedProfile = profile; Assert.NotNull(vm.ClientLogoPreview); diff --git a/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs b/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs new file mode 100644 index 0000000..dcfb981 --- /dev/null +++ b/SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs @@ -0,0 +1,157 @@ +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SharepointToolbox.Core.Models; +using SharepointToolbox.Infrastructure.Auth; +using SharepointToolbox.Infrastructure.Persistence; +using SharepointToolbox.Services; +using SharepointToolbox.ViewModels; + +namespace SharepointToolbox.Tests.ViewModels; + +[Trait("Category", "Unit")] +public class ProfileManagementViewModelRegistrationTests : IDisposable +{ + private readonly string _tempFile; + private readonly Mock _mockBranding; + private readonly Mock _mockAppReg; + private readonly GraphClientFactory _graphClientFactory; + + public ProfileManagementViewModelRegistrationTests() + { + _tempFile = Path.GetTempFileName(); + File.Delete(_tempFile); + _mockBranding = new Mock(); + _mockAppReg = new Mock(); + _graphClientFactory = new GraphClientFactory(new MsalClientFactory()); + } + + public void Dispose() + { + if (File.Exists(_tempFile)) File.Delete(_tempFile); + if (File.Exists(_tempFile + ".tmp")) File.Delete(_tempFile + ".tmp"); + } + + private ProfileManagementViewModel CreateViewModel() + { + var profileService = new ProfileService(new ProfileRepository(_tempFile)); + return new ProfileManagementViewModel( + profileService, + _mockBranding.Object, + _graphClientFactory, + NullLogger.Instance, + _mockAppReg.Object); + } + + private static TenantProfile MakeProfile(string? appId = null) => new TenantProfile + { + Name = "TestTenant", + TenantUrl = "https://test.sharepoint.com", + ClientId = "00000000-0000-0000-0000-000000000001", + AppId = appId + }; + + [Fact] + public void RegisterAppCommand_CanExecute_WhenProfileSelected_AndNoAppId() + { + var vm = CreateViewModel(); + vm.SelectedProfile = MakeProfile(appId: null); + + Assert.True(vm.RegisterAppCommand.CanExecute(null)); + } + + [Fact] + public void RegisterAppCommand_CannotExecute_WhenNoProfile() + { + var vm = CreateViewModel(); + + Assert.False(vm.RegisterAppCommand.CanExecute(null)); + } + + [Fact] + public void RemoveAppCommand_CanExecute_WhenProfileHasAppId() + { + var vm = CreateViewModel(); + vm.SelectedProfile = MakeProfile(appId: "some-app-id"); + + Assert.True(vm.RemoveAppCommand.CanExecute(null)); + } + + [Fact] + public void RemoveAppCommand_CannotExecute_WhenNoAppId() + { + var vm = CreateViewModel(); + vm.SelectedProfile = MakeProfile(appId: null); + + Assert.False(vm.RemoveAppCommand.CanExecute(null)); + } + + [Fact] + public async Task RegisterApp_ShowsFallback_WhenNotAdmin() + { + _mockAppReg + .Setup(s => s.IsGlobalAdminAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + var vm = CreateViewModel(); + vm.SelectedProfile = MakeProfile(appId: null); + + await vm.RegisterAppCommand.ExecuteAsync(null); + + Assert.True(vm.ShowFallbackInstructions); + } + + [Fact] + public async Task RegisterApp_SetsAppId_OnSuccess() + { + _mockAppReg + .Setup(s => s.IsGlobalAdminAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _mockAppReg + .Setup(s => s.RegisterAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(AppRegistrationResult.Success("new-app-id-123")); + + var profileService = new ProfileService(new ProfileRepository(_tempFile)); + var profile = MakeProfile(appId: null); + await profileService.AddProfileAsync(profile); + + var vm = new ProfileManagementViewModel( + profileService, + _mockBranding.Object, + _graphClientFactory, + NullLogger.Instance, + _mockAppReg.Object); + vm.SelectedProfile = profile; + + await vm.RegisterAppCommand.ExecuteAsync(null); + + Assert.Equal("new-app-id-123", profile.AppId); + } + + [Fact] + public async Task RemoveApp_ClearsAppId() + { + _mockAppReg + .Setup(s => s.RemoveAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + _mockAppReg + .Setup(s => s.ClearMsalSessionAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var profileService = new ProfileService(new ProfileRepository(_tempFile)); + var profile = MakeProfile(appId: "existing-app-id"); + await profileService.AddProfileAsync(profile); + + var vm = new ProfileManagementViewModel( + profileService, + _mockBranding.Object, + _graphClientFactory, + NullLogger.Instance, + _mockAppReg.Object); + vm.SelectedProfile = profile; + + await vm.RemoveAppCommand.ExecuteAsync(null); + + Assert.Null(profile.AppId); + } +} diff --git a/SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml b/SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml index ba1ce9f..aca0801 100644 --- a/SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml +++ b/SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml @@ -2,9 +2,12 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:loc="clr-namespace:SharepointToolbox.Localization" - Title="Manage Profiles" Width="500" Height="620" + Title="Manage Profiles" Width="500" Height="750" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" ResizeMode="NoResize"> + + + @@ -12,6 +15,7 @@ + @@ -83,8 +87,47 @@ Visibility="{Binding ValidationMessage, Converter={StaticResource StringToVisibilityConverter}}" /> + + +