using Sbroenne.ExcelMcp.CLI.Infrastructure;
using Xunit;
namespace Sbroenne.ExcelMcp.CLI.Tests.Integration;
/// <summary>
/// Integration tests for version check notification workflow.
/// These tests verify the end-to-end flow from NuGet check to notification message generation.
/// </summary>
[Trait("Layer", "CLI")]
[Trait("Category", "Integration")]
[Trait("Feature", "VersionCheck")]
[Trait("Speed", "Medium")]
public sealed class VersionCheckNotificationTests
{
/// <summary>
/// Verifies that the full version check workflow completes without error
/// and produces valid notification content when an update is available.
/// </summary>
[Fact]
public async Task VersionCheckWorkflow_WhenUpdateAvailable_ProducesValidNotification()
{
// Act - Run the version check (this actually hits NuGet)
var updateInfo = await DaemonVersionChecker.CheckForUpdateAsync();
// Assert - If an update is available, verify the notification content is valid
if (updateInfo != null)
{
// Verify notification title
var title = UpdateInfo.GetNotificationTitle();
Assert.False(string.IsNullOrWhiteSpace(title), "Notification title should not be empty");
Assert.Contains("Update", title, StringComparison.OrdinalIgnoreCase);
// Verify notification message contains essential information
var message = updateInfo.GetNotificationMessage();
Assert.False(string.IsNullOrWhiteSpace(message), "Notification message should not be empty");
Assert.Contains(updateInfo.CurrentVersion, message);
Assert.Contains(updateInfo.LatestVersion, message);
Assert.Contains("dotnet tool update", message);
Assert.Contains("Sbroenne.ExcelMcp.CLI", message);
// Verify versions are valid semver-like strings
Assert.Matches(@"^\d+\.\d+", updateInfo.CurrentVersion);
Assert.Matches(@"^\d+\.\d+", updateInfo.LatestVersion);
}
// If no update available, that's also valid - test passes
}
/// <summary>
/// Verifies that the version check handles network timeouts gracefully
/// and produces no notification (null result).
/// </summary>
[Fact]
public async Task VersionCheckWorkflow_NetworkTimeout_ProducesNoNotification()
{
// Arrange - Create a cancellation token that expires immediately
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1));
// Act - Run the version check with immediate timeout
var updateInfo = await DaemonVersionChecker.CheckForUpdateAsync(cts.Token);
// Assert - Should return null (no notification) on timeout
Assert.Null(updateInfo);
}
/// <summary>
/// Verifies that multiple concurrent version checks don't interfere with each other.
/// This simulates the daemon startup scenario where multiple components might check.
/// </summary>
[Fact]
public async Task VersionCheckWorkflow_ConcurrentChecks_AllComplete()
{
// Arrange - Start multiple concurrent checks
var tasks = new List<Task<UpdateInfo?>>
{
DaemonVersionChecker.CheckForUpdateAsync(),
DaemonVersionChecker.CheckForUpdateAsync(),
DaemonVersionChecker.CheckForUpdateAsync()
};
// Act - Wait for all to complete
var results = await Task.WhenAll(tasks);
// Assert - All should complete (either null or valid UpdateInfo)
Assert.Equal(3, results.Length);
// All results should be consistent (same state)
var nonNullResults = results.Where(r => r != null).ToList();
if (nonNullResults.Count > 1)
{
// If multiple returned update info, they should match
var first = nonNullResults[0]!;
foreach (var result in nonNullResults.Skip(1))
{
Assert.Equal(first.LatestVersion, result!.LatestVersion);
}
}
}
/// <summary>
/// Verifies that the notification message length is reasonable for Windows balloon tips.
/// Windows balloon tips have a ~256 character limit for the message.
/// </summary>
[Fact]
public void NotificationMessage_Length_FitsWindowsBalloonTip()
{
// Arrange - Create update info with realistic versions
var updateInfo = new UpdateInfo
{
CurrentVersion = "1.5.14",
LatestVersion = "99.99.99", // Worst case: large version numbers
UpdateAvailable = true
};
// Act
var title = UpdateInfo.GetNotificationTitle();
var message = updateInfo.GetNotificationMessage();
// Assert - Windows balloon tip limits
// Title: 63 characters max (truncated by Windows if longer)
// Message: 255 characters max (truncated by Windows if longer)
Assert.True(title.Length <= 63, $"Title too long ({title.Length} chars): {title}");
Assert.True(message.Length <= 255, $"Message too long ({message.Length} chars): {message}");
}
/// <summary>
/// Verifies that the notification content doesn't contain problematic characters
/// that could cause issues with Windows notifications.
/// </summary>
[Fact]
public void NotificationContent_NoProblematicCharacters()
{
// Arrange
var updateInfo = new UpdateInfo
{
CurrentVersion = "1.0.0",
LatestVersion = "2.0.0",
UpdateAvailable = true
};
// Act
var title = UpdateInfo.GetNotificationTitle();
var message = updateInfo.GetNotificationMessage();
// Assert - No control characters (except newline which is OK in message)
Assert.DoesNotMatch(@"[\x00-\x09\x0B\x0C\x0E-\x1F]", title);
Assert.DoesNotMatch(@"[\x00-\x09\x0B\x0C\x0E-\x1F]", message);
// Verify no embedded null characters (check each char)
Assert.DoesNotContain('\0', title);
Assert.DoesNotContain('\0', message);
}
/// <summary>
/// Verifies that version comparison works correctly for various version formats.
/// </summary>
[Theory]
[InlineData("1.0.0", "2.0.0", true)] // Major version increase
[InlineData("1.0.0", "1.1.0", true)] // Minor version increase
[InlineData("1.0.0", "1.0.1", true)] // Patch version increase
[InlineData("1.5.14", "1.5.15", true)] // Realistic scenario
[InlineData("2.0.0", "1.0.0", false)] // Downgrade (no update)
[InlineData("1.0.0", "1.0.0", false)] // Same version (no update)
public void VersionComparison_VariousScenarios_CorrectResult(
string current, string latest, bool expectUpdate)
{
// This tests the version comparison logic indirectly through UpdateInfo
// The actual comparison happens in DaemonVersionChecker.CompareVersions
// but we verify the expected behavior through the UpdateAvailable flag
var updateInfo = new UpdateInfo
{
CurrentVersion = current,
LatestVersion = latest,
UpdateAvailable = expectUpdate
};
// Verify the message reflects the update status
var message = updateInfo.GetNotificationMessage();
Assert.Contains(latest, message);
Assert.Contains(current, message);
}
/// <summary>
/// Verifies that the fire-and-forget pattern doesn't block.
/// This simulates the daemon startup behavior.
/// </summary>
[Fact]
public async Task FireAndForgetPattern_DoesNotBlock()
{
// Arrange
var startTime = DateTime.UtcNow;
var checkStarted = false;
// Act - Fire and forget pattern (like daemon startup)
_ = Task.Run(async () =>
{
checkStarted = true;
await Task.Delay(TimeSpan.FromMilliseconds(100)); // Simulate delay
await DaemonVersionChecker.CheckForUpdateAsync();
});
// This should return immediately (not blocked by the version check)
var afterFireTime = DateTime.UtcNow;
// Assert - Fire-and-forget should return immediately
var fireTime = (afterFireTime - startTime).TotalMilliseconds;
Assert.True(fireTime < 50, $"Fire-and-forget took {fireTime}ms, should be nearly instant");
// Wait for the background task to complete
await Task.Delay(TimeSpan.FromSeconds(2));
// Verify the task did run
Assert.True(checkStarted, "Background task should have started");
}
}