fix(07): fix people picker selection and audit service authentication

People picker ListBox used MouseBinding which fires before SelectedItem
updates, causing null CommandParameter. Replaced with SelectionChanged
event handler in code-behind.

AuditUsersAsync created TenantProfile with empty ClientId, causing
ArgumentException in SessionManager. Added currentProfile parameter
to pass the authenticated tenant's ClientId through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-07 13:44:53 +02:00
parent 0af73df65c
commit 00252fd137
7 changed files with 57 additions and 8 deletions

View File

@@ -61,6 +61,13 @@ public class UserAccessAuditServiceTests
FolderDepth: 1,
IncludeSubsites: false);
private static TenantProfile DefaultProfile => new()
{
Name = "Test",
TenantUrl = "https://contoso.sharepoint.com",
ClientId = "test-client-id"
};
// ── Test 1: Filter by target user login ───────────────────────────────────
[Fact]
@@ -77,6 +84,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -103,6 +111,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -127,6 +136,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -151,6 +161,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -175,6 +186,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -199,6 +211,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -223,6 +236,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -248,6 +262,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { extLogin },
new[] { MakeSite() },
DefaultOptions,
@@ -272,6 +287,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@x.com", "bob@x.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -298,6 +314,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
new[] { MakeSite() },
DefaultOptions,
@@ -324,6 +341,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
session.Object,
DefaultProfile,
Array.Empty<string>(),
new[] { MakeSite() },
DefaultOptions,
@@ -367,6 +385,7 @@ public class UserAccessAuditServiceTests
var result = await svc.AuditUsersAsync(
mockSession.Object,
DefaultProfile,
new[] { "alice@contoso.com" },
sites,
DefaultOptions,

View File

@@ -49,6 +49,7 @@ public class UserAccessAuditViewModelTests
mockAudit
.Setup(s => s.AuditUsersAsync(
It.IsAny<ISessionManager>(),
It.IsAny<TenantProfile>(),
It.IsAny<IReadOnlyList<string>>(),
It.IsAny<IReadOnlyList<SiteInfo>>(),
It.IsAny<ScanOptions>(),
@@ -65,6 +66,14 @@ public class UserAccessAuditViewModelTests
mockSession.Object,
NullLogger<FeatureViewModelBase>.Instance);
// Set a default profile so RunOperationAsync doesn't early-return
vm._currentProfile = new TenantProfile
{
Name = "Test",
TenantUrl = "https://contoso.sharepoint.com",
ClientId = "test-client-id"
};
return (vm, mockAudit, mockGraph);
}
@@ -83,6 +92,7 @@ public class UserAccessAuditViewModelTests
auditMock.Verify(
s => s.AuditUsersAsync(
It.IsAny<ISessionManager>(),
It.IsAny<TenantProfile>(),
It.IsAny<IReadOnlyList<string>>(),
It.IsAny<IReadOnlyList<SiteInfo>>(),
It.IsAny<ScanOptions>(),

View File

@@ -22,6 +22,7 @@ public interface IUserAccessAuditService
/// <returns>Flat list of access entries for the target users.</returns>
Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
ISessionManager sessionManager,
TenantProfile currentProfile,
IReadOnlyList<string> targetUserLogins,
IReadOnlyList<SiteInfo> sites,
ScanOptions options,

View File

@@ -24,6 +24,7 @@ public class UserAccessAuditService : IUserAccessAuditService
public async Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
ISessionManager sessionManager,
TenantProfile currentProfile,
IReadOnlyList<string> targetUserLogins,
IReadOnlyList<SiteInfo> sites,
ScanOptions options,
@@ -53,7 +54,7 @@ public class UserAccessAuditService : IUserAccessAuditService
var profile = new TenantProfile
{
TenantUrl = site.Url,
ClientId = string.Empty, // Will be set by SessionManager from cached session
ClientId = currentProfile.ClientId,
Name = site.Title
};

View File

@@ -229,8 +229,15 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
FolderDepth: 1,
IncludeSubsites: IncludeSubsites);
if (_currentProfile == null)
{
StatusMessage = "No tenant profile selected. Please connect first.";
return;
}
var entries = await _auditService.AuditUsersAsync(
_sessionManager,
_currentProfile,
userLogins,
effectiveSites,
scanOptions,

View File

@@ -31,8 +31,10 @@
</Style>
</TextBlock.Style>
</TextBlock>
<ListBox ItemsSource="{Binding SearchResults}" MaxHeight="120"
ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,4">
<ListBox x:Name="SearchResultsListBox"
ItemsSource="{Binding SearchResults}" MaxHeight="120"
ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,4"
SelectionChanged="SearchResultsListBox_SelectionChanged">
<ListBox.Style>
<Style TargetType="ListBox">
<Style.Triggers>
@@ -54,11 +56,6 @@
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.InputBindings>
<MouseBinding MouseAction="LeftClick"
Command="{Binding AddUserCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=SelectedItem}" />
</ListBox.InputBindings>
</ListBox>
<ItemsControl ItemsSource="{Binding SelectedUsers}" Margin="0,0,0,4">
<ItemsControl.ItemTemplate>

View File

@@ -1,4 +1,5 @@
using System.Windows.Controls;
using SharepointToolbox.Services;
using SharepointToolbox.ViewModels.Tabs;
namespace SharepointToolbox.Views.Tabs;
@@ -10,4 +11,17 @@ public partial class UserAccessAuditView : UserControl
InitializeComponent();
DataContext = viewModel;
}
private void SearchResultsListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox && listBox.SelectedItem is GraphUserResult user)
{
var vm = (UserAccessAuditViewModel)DataContext;
if (vm.AddUserCommand.CanExecute(user))
vm.AddUserCommand.Execute(user);
// Clear selection so the same item can be re-selected if needed
listBox.SelectedItem = null;
}
}
}