Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
35d83f9
Add V4 provider DB schema with ResolvedFromOwningPublisher and merge …
jschick04 Apr 30, 2026
ae4e4f5
Distinguish unrecognized vs obsolete provider DB formats in EventReso…
jschick04 Apr 30, 2026
6eff73d
Reject obsolete and unrecognized provider DBs in EventDbTool commands
jschick04 Apr 30, 2026
8b07f3c
Add database entry, status, and import result models
jschick04 Apr 30, 2026
933db26
Restructure DatabaseService to single-owner model with per-entry import
jschick04 Apr 30, 2026
52f6469
Buffer SettingsModal toggles and surface per-entry import failures
jschick04 Apr 30, 2026
20b96d7
Treat ProviderDetails-less databases as Unknown schema
jschick04 Apr 30, 2026
2776e46
Quarantine empty and unrecognized provider databases at classificatio…
jschick04 Apr 30, 2026
d66eeff
Default DatabaseEntry status to NotClassified and surface BackupExists
jschick04 May 1, 2026
60af27e
Expose non-faulting InitialClassificationTask on IDatabaseService
jschick04 May 1, 2026
6e704d6
Detect interrupted upgrades during classification via .upgrade.bak ma…
jschick04 May 1, 2026
0971893
Add safe recovery file operations to DatabaseService
jschick04 May 1, 2026
a46c235
Stop Remove from deleting user-created .bak files via wildcard
jschick04 May 1, 2026
c4e68f6
Rename BannerService Critical/Error to align with severity taxonomy
jschick04 May 1, 2026
0420221
Add optional action button support to error banner
jschick04 May 1, 2026
68bf136
Add Channel-based UpgradeBatchAsync to DatabaseService with progress …
jschick04 May 2, 2026
5c31bc2
Add UpgradeProgress slice to BannerService driven by upgrade-batch ev…
jschick04 May 2, 2026
9eea7e3
Add Attention slice to BannerService with reset-on-growth dismiss sem…
jschick04 May 2, 2026
3ebe11f
Render attention and upgrade-progress slices in BannerHost with flat-…
jschick04 May 2, 2026
11dd109
Move ModalChrome and FooterPreset from Shared/Base to EventLogExpert.…
jschick04 May 2, 2026
576d10a
Add DatabaseRecoveryDialog using ModalChrome with FooterDisabled support
jschick04 May 2, 2026
7d1f6c0
Add DatabaseRecoveryHost to surface banner and open recovery dialog f…
jschick04 May 2, 2026
250a720
Add inline upgrade banner and trigger settings-scope upgrades from Se…
jschick04 May 2, 2026
4a914c1
Default freshly-imported databases to disabled and auto-upgrade V3 en…
jschick04 May 2, 2026
3627f06
Guard HandleOpenLog with classification await and resolver try/catch
jschick04 May 2, 2026
cf04995
Add classification-pending UX in SettingsModal and tighten WCAG AA co…
jschick04 May 2, 2026
ad7298d
Restructure SettingsModal database row UX with per-status primary act…
jschick04 May 3, 2026
78effa3
Tighten settings modal chrome and database row visuals
jschick04 May 3, 2026
c1c6e06
Reveal database row trash via name click with recessed left strip
jschick04 May 3, 2026
406171f
Narrow database CLI exception handling to probe-time and share schema…
jschick04 May 3, 2026
c145edc
Skip schema auto-create on probe and pluralize recovery dialog copy
jschick04 May 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
610 changes: 549 additions & 61 deletions src/EventLogExpert.Components.Tests/BannerHostTests.cs

Large diffs are not rendered by default.

430 changes: 430 additions & 0 deletions src/EventLogExpert.Components.Tests/DatabaseEntryRowTests.cs

Large diffs are not rendered by default.

580 changes: 580 additions & 0 deletions src/EventLogExpert.Components.Tests/DatabaseRecoveryDialogTests.cs

Large diffs are not rendered by default.

405 changes: 405 additions & 0 deletions src/EventLogExpert.Components.Tests/DatabaseRecoveryHostTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using Bunit;
using EventLogExpert.Eventing.Helpers;
using EventLogExpert.UI.Interfaces;
using EventLogExpert.UI.Models;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

namespace EventLogExpert.Components.Tests;

public sealed class SettingsUpgradeProgressBannerTests : BunitContext
{
private readonly IBannerService _bannerService = Substitute.For<IBannerService>();
private readonly ITraceLogger _traceLogger = Substitute.For<ITraceLogger>();

public SettingsUpgradeProgressBannerTests()
{
_bannerService.SettingsProgress.Returns((BannerProgressEntry?)null);
_bannerService.BackgroundProgress.Returns((BannerProgressEntry?)null);

Services.AddSingleton(_bannerService);
Services.AddSingleton(_traceLogger);

JSInterop.Mode = JSRuntimeMode.Loose;
}

[Fact]
public void SettingsUpgradeProgressBanner_BackgroundProgressOnly_RendersNothing()
{
// Arrange
_bannerService.BackgroundProgress.Returns(BuildProgress(UpgradeProgressScope.Background));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
Assert.Empty(component.FindAll("aside.settings-upgrade-banner"));
}

[Fact]
public async Task SettingsUpgradeProgressBanner_CancelClicked_InvokesProgressCancelDelegate()
{
// Arrange
int cancelInvocationCount = 0;
_bannerService.SettingsProgress.Returns(BuildProgress(cancel: () => cancelInvocationCount++));

// Act
var component = Render<SettingsUpgradeProgressBanner>();
await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new());

// Assert
Assert.Equal(1, cancelInvocationCount);
}

[Fact]
public async Task SettingsUpgradeProgressBanner_CancelThrows_LogsViaTraceLoggerAndDoesNotPropagate()
{
// Arrange
_bannerService.SettingsProgress.Returns(
BuildProgress(cancel: () => throw new InvalidOperationException("cts disposed")));

// Act
var component = Render<SettingsUpgradeProgressBanner>();
await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new());

// Assert
Assert.Single(component.FindAll("aside.settings-upgrade-banner"));

_traceLogger.Received(1).Error(Arg.Is<ErrorLogHandler>(h =>
h.ToString().Contains(nameof(SettingsUpgradeProgressBanner)) &&
h.ToString().Contains("cts disposed")));
}

[Fact]
public void SettingsUpgradeProgressBanner_DisposeIsIdempotent()
{
// Arrange
var component = Render<SettingsUpgradeProgressBanner>();
var instance = component.Instance;

// Act + Assert
instance.Dispose();
instance.Dispose();

}

[Fact]
public async Task SettingsUpgradeProgressBanner_DisposeUnsubscribesFromStateChanged()
{
// Arrange
_bannerService.SettingsProgress.Returns((BannerProgressEntry?)null);

var component = Render<SettingsUpgradeProgressBanner>();
component.Instance.Dispose();

// Act
_bannerService.SettingsProgress.Returns(BuildProgress());
_bannerService.StateChanged += Raise.Event<Action>();

await Task.Yield();

// Assert
Assert.Empty(component.FindAll("aside.settings-upgrade-banner"));
}

[Fact]
public void SettingsUpgradeProgressBanner_QueuedBatchesAfterOne_UsesSingularBatchLabel()
{
// Arrange
_bannerService.SettingsProgress.Returns(BuildProgress(queuedBatchesAfter: 1));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
var subtitle = component.Find("aside.settings-upgrade-banner .banner-subtitle");
Assert.Contains("+1 batch queued", subtitle.TextContent);
Assert.DoesNotContain("batches", subtitle.TextContent);
}

[Fact]
public void SettingsUpgradeProgressBanner_QueuedBatchesAfterTwo_UsesPluralBatchesLabel()
{
// Arrange
_bannerService.SettingsProgress.Returns(BuildProgress(queuedBatchesAfter: 2));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
var subtitle = component.Find("aside.settings-upgrade-banner .banner-subtitle");
Assert.Contains("+2 batches queued", subtitle.TextContent);
}

[Fact]
public void SettingsUpgradeProgressBanner_QueuedBatchesAfterZero_DoesNotRenderSubtitle()
{
// Arrange
_bannerService.SettingsProgress.Returns(BuildProgress(queuedBatchesAfter: 0));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
Assert.Empty(component.FindAll("aside.settings-upgrade-banner .banner-subtitle"));
}

[Fact]
public void SettingsUpgradeProgressBanner_SettingsProgressNull_RendersNothing()
{
// Arrange
_bannerService.SettingsProgress.Returns((BannerProgressEntry?)null);

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
Assert.Empty(component.FindAll("aside.settings-upgrade-banner"));
}

[Fact]
public void SettingsUpgradeProgressBanner_SettingsProgressWithBatchSizeOne_UsesSingularDatabaseLabel()
{
// Arrange
_bannerService.SettingsProgress.Returns(
BuildProgress(currentBatchSize: 1, currentEntryName: string.Empty));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
var banner = component.Find("aside.settings-upgrade-banner");
Assert.Contains("Preparing upgrade of 1 database", banner.TextContent);
Assert.DoesNotContain("databases", banner.TextContent);
}

[Fact]
public void SettingsUpgradeProgressBanner_SettingsProgressWithEmptyEntryName_RendersPreparingMessage()
{
// Arrange
_bannerService.SettingsProgress.Returns(
BuildProgress(currentBatchSize: 3, currentEntryName: string.Empty));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
var banner = component.Find("aside.settings-upgrade-banner");
Assert.Contains("Preparing upgrade of 3 databases", banner.TextContent);
Assert.DoesNotContain("Upgrading database 0", banner.TextContent);
}

[Fact]
public void
SettingsUpgradeProgressBanner_SettingsProgressWithEntryName_RendersUpgradeProgressBannerWithCancelButton()
{
// Arrange
_bannerService.SettingsProgress.Returns(BuildProgress(
currentBatchPosition: 2,
currentBatchSize: 5,
currentEntryName: "MyDb.evtx",
currentPhase: UpgradePhase.MigratingSchema));

// Act
var component = Render<SettingsUpgradeProgressBanner>();

// Assert
var banner = component.Find("aside.settings-upgrade-banner");
Assert.Contains("Upgrading database 2 of 5", banner.TextContent);
Assert.Contains("MyDb.evtx", banner.TextContent);
Assert.Contains("MigratingSchema", banner.TextContent);
Assert.Equal("Cancel", component.Find("aside.settings-upgrade-banner button.banner-action").TextContent.Trim());
Assert.Single(component.FindAll("aside.settings-upgrade-banner .banner-spinner"));
}

[Fact]
public async Task SettingsUpgradeProgressBanner_StateChangedRaised_RerendersWithNewProgress()
{
// Arrange
_bannerService.SettingsProgress.Returns((BannerProgressEntry?)null);
var component = Render<SettingsUpgradeProgressBanner>();
Assert.Empty(component.FindAll("aside.settings-upgrade-banner"));

// Act
_bannerService.SettingsProgress.Returns(BuildProgress(currentEntryName: "x.evtx"));
_bannerService.StateChanged += Raise.Event<Action>();

// Assert
await component.WaitForAssertionAsync(() =>
Assert.Single(component.FindAll("aside.settings-upgrade-banner")));
}

private static BannerProgressEntry BuildProgress(
UpgradeProgressScope scope = UpgradeProgressScope.SettingsTriggered,
int currentBatchPosition = 1,
int currentBatchSize = 1,
string currentEntryName = "x.evtx",
UpgradePhase currentPhase = UpgradePhase.MigratingSchema,
int queuedBatchesAfter = 0,
Action? cancel = null) =>
new(
Guid.NewGuid(),
scope,
currentBatchPosition,
currentBatchSize,
currentEntryName,
currentPhase,
queuedBatchesAfter,
cancel ?? (() => { }));
}
Loading
Loading