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
This commit is contained in:
@@ -15,6 +15,7 @@ public class ProfileManagementViewModelLogoTests : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly string _tempFile;
|
private readonly string _tempFile;
|
||||||
private readonly Mock<IBrandingService> _mockBranding;
|
private readonly Mock<IBrandingService> _mockBranding;
|
||||||
|
private readonly Mock<IAppRegistrationService> _mockAppReg;
|
||||||
private readonly GraphClientFactory _graphClientFactory;
|
private readonly GraphClientFactory _graphClientFactory;
|
||||||
private readonly ILogger<ProfileManagementViewModel> _logger;
|
private readonly ILogger<ProfileManagementViewModel> _logger;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ public class ProfileManagementViewModelLogoTests : IDisposable
|
|||||||
_tempFile = Path.GetTempFileName();
|
_tempFile = Path.GetTempFileName();
|
||||||
File.Delete(_tempFile);
|
File.Delete(_tempFile);
|
||||||
_mockBranding = new Mock<IBrandingService>();
|
_mockBranding = new Mock<IBrandingService>();
|
||||||
|
_mockAppReg = new Mock<IAppRegistrationService>();
|
||||||
_graphClientFactory = new GraphClientFactory(new MsalClientFactory());
|
_graphClientFactory = new GraphClientFactory(new MsalClientFactory());
|
||||||
_logger = NullLogger<ProfileManagementViewModel>.Instance;
|
_logger = NullLogger<ProfileManagementViewModel>.Instance;
|
||||||
}
|
}
|
||||||
@@ -40,7 +42,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable
|
|||||||
profileService,
|
profileService,
|
||||||
_mockBranding.Object,
|
_mockBranding.Object,
|
||||||
_graphClientFactory,
|
_graphClientFactory,
|
||||||
_logger);
|
_logger,
|
||||||
|
_mockAppReg.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -102,7 +105,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable
|
|||||||
profileService,
|
profileService,
|
||||||
_mockBranding.Object,
|
_mockBranding.Object,
|
||||||
_graphClientFactory,
|
_graphClientFactory,
|
||||||
_logger);
|
_logger,
|
||||||
|
_mockAppReg.Object);
|
||||||
|
|
||||||
vm.SelectedProfile = profile;
|
vm.SelectedProfile = profile;
|
||||||
|
|
||||||
@@ -173,7 +177,8 @@ public class ProfileManagementViewModelLogoTests : IDisposable
|
|||||||
profileService,
|
profileService,
|
||||||
_mockBranding.Object,
|
_mockBranding.Object,
|
||||||
_graphClientFactory,
|
_graphClientFactory,
|
||||||
_logger);
|
_logger,
|
||||||
|
_mockAppReg.Object);
|
||||||
|
|
||||||
vm.SelectedProfile = profile;
|
vm.SelectedProfile = profile;
|
||||||
Assert.NotNull(vm.ClientLogoPreview);
|
Assert.NotNull(vm.ClientLogoPreview);
|
||||||
|
|||||||
@@ -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<IBrandingService> _mockBranding;
|
||||||
|
private readonly Mock<IAppRegistrationService> _mockAppReg;
|
||||||
|
private readonly GraphClientFactory _graphClientFactory;
|
||||||
|
|
||||||
|
public ProfileManagementViewModelRegistrationTests()
|
||||||
|
{
|
||||||
|
_tempFile = Path.GetTempFileName();
|
||||||
|
File.Delete(_tempFile);
|
||||||
|
_mockBranding = new Mock<IBrandingService>();
|
||||||
|
_mockAppReg = new Mock<IAppRegistrationService>();
|
||||||
|
_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<ProfileManagementViewModel>.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<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.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<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
_mockAppReg
|
||||||
|
.Setup(s => s.RegisterAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.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<ProfileManagementViewModel>.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<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_mockAppReg
|
||||||
|
.Setup(s => s.ClearMsalSessionAsync(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.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<ProfileManagementViewModel>.Instance,
|
||||||
|
_mockAppReg.Object);
|
||||||
|
vm.SelectedProfile = profile;
|
||||||
|
|
||||||
|
await vm.RemoveAppCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Null(profile.AppId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
|
xmlns:loc="clr-namespace:SharepointToolbox.Localization"
|
||||||
Title="Manage Profiles" Width="500" Height="620"
|
Title="Manage Profiles" Width="500" Height="750"
|
||||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
|
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
|
||||||
ResizeMode="NoResize">
|
ResizeMode="NoResize">
|
||||||
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||||
|
</Window.Resources>
|
||||||
<Grid Margin="12">
|
<Grid Margin="12">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -12,6 +15,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Profile list -->
|
<!-- Profile list -->
|
||||||
@@ -83,8 +87,47 @@
|
|||||||
Visibility="{Binding ValidationMessage, Converter={StaticResource StringToVisibilityConverter}}" />
|
Visibility="{Binding ValidationMessage, Converter={StaticResource StringToVisibilityConverter}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- App Registration -->
|
||||||
|
<StackPanel Grid.Row="4" Margin="0,8,0,8">
|
||||||
|
<Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.title]}"
|
||||||
|
FontWeight="SemiBold" Padding="0,0,0,4"
|
||||||
|
Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||||
|
|
||||||
|
<!-- Register / Remove buttons -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.register]}"
|
||||||
|
Command="{Binding RegisterAppCommand}" Width="120" Margin="0,0,8,0" />
|
||||||
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.remove]}"
|
||||||
|
Command="{Binding RemoveAppCommand}" Width="120" Margin="0,0,8,0" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Status text -->
|
||||||
|
<TextBlock Text="{Binding RegistrationStatus}" FontSize="11" Margin="0,2,0,0"
|
||||||
|
Foreground="#006600"
|
||||||
|
Visibility="{Binding RegistrationStatus, Converter={StaticResource StringToVisibilityConverter}}" />
|
||||||
|
|
||||||
|
<!-- Fallback instructions panel -->
|
||||||
|
<Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4" Margin="0,4,0,0"
|
||||||
|
Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step1]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step2]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step3]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step4]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step5]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step6]}"
|
||||||
|
TextWrapping="Wrap" Margin="0,2" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.add]}"
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.add]}"
|
||||||
Command="{Binding AddCommand}" Width="60" Margin="4,0" />
|
Command="{Binding AddCommand}" Width="60" Margin="4,0" />
|
||||||
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.rename]}"
|
<Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.rename]}"
|
||||||
|
|||||||
Reference in New Issue
Block a user