119 lines
4.3 KiB
C#
119 lines
4.3 KiB
C#
|
|
using System.IO;
|
||
|
|
using FluentAssertions;
|
||
|
|
using TeamsISO.App.Services;
|
||
|
|
|
||
|
|
namespace TeamsISO.App.Tests.Services;
|
||
|
|
|
||
|
|
// UpdateChecker unit tests.
|
||
|
|
//
|
||
|
|
// We don't exercise CheckAsync (the real HTTP call against Forgejo) —
|
||
|
|
// tests must not depend on the network. Coverage instead:
|
||
|
|
// • TryParseSemVer: version-comparison parsing across the inputs the
|
||
|
|
// real release stream produces.
|
||
|
|
// • CheckIfDueAsync throttle: a recent cooldown stamp short-circuits
|
||
|
|
// and returns null *before* CheckAsync runs (which would otherwise
|
||
|
|
// fire an HTTP request).
|
||
|
|
public sealed class UpdateCheckerTests : IDisposable
|
||
|
|
{
|
||
|
|
private readonly string _tempDir;
|
||
|
|
private readonly string? _previousOverride;
|
||
|
|
|
||
|
|
public UpdateCheckerTests()
|
||
|
|
{
|
||
|
|
_tempDir = Path.Combine(Path.GetTempPath(), $"teamsiso-update-{Guid.NewGuid():N}");
|
||
|
|
Directory.CreateDirectory(_tempDir);
|
||
|
|
_previousOverride = UpdateChecker.StateDirectoryOverride;
|
||
|
|
UpdateChecker.StateDirectoryOverride = _tempDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
UpdateChecker.StateDirectoryOverride = _previousOverride;
|
||
|
|
try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, recursive: true); }
|
||
|
|
catch { /* cleanup best-effort */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
[Theory]
|
||
|
|
[InlineData("v1.2.3", "1.2.3")]
|
||
|
|
[InlineData("V1.2.3", "1.2.3")] // case-insensitive 'v' strip
|
||
|
|
[InlineData("1.2.3", "1.2.3")]
|
||
|
|
[InlineData("v1.2.3.4", "1.2.3.4")] // 4-segment .NET-style versions
|
||
|
|
[InlineData("v1.2.3-alpha", "1.2.3")] // pre-release suffix stripped
|
||
|
|
[InlineData("v1.2.3-beta.4", "1.2.3")]
|
||
|
|
public void TryParseSemVer_AcceptsExpectedForms(string input, string expected)
|
||
|
|
{
|
||
|
|
UpdateChecker.TryParseSemVer(input).Should().Be(Version.Parse(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Theory]
|
||
|
|
[InlineData("not-a-version")]
|
||
|
|
[InlineData("v.invalid")]
|
||
|
|
[InlineData("")]
|
||
|
|
public void TryParseSemVer_ReturnsNullOnGarbage(string input)
|
||
|
|
{
|
||
|
|
UpdateChecker.TryParseSemVer(input).Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void TryParseSemVer_OrderingIsSemantic()
|
||
|
|
{
|
||
|
|
// The CheckAsync comparison is "latest > current" — pin the
|
||
|
|
// ordering across the version arc the release process actually
|
||
|
|
// produces.
|
||
|
|
var older = UpdateChecker.TryParseSemVer("v0.1.0")!;
|
||
|
|
var newer = UpdateChecker.TryParseSemVer("v0.2.0")!;
|
||
|
|
var newest = UpdateChecker.TryParseSemVer("v1.0.0")!;
|
||
|
|
|
||
|
|
(newer > older).Should().BeTrue();
|
||
|
|
(newest > newer).Should().BeTrue();
|
||
|
|
(newest > older).Should().BeTrue();
|
||
|
|
(older > newer).Should().BeFalse();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CheckIfDueAsync_ReturnsNull_WhenCooldownStampIsRecent()
|
||
|
|
{
|
||
|
|
// Pre-write a "we just checked" stamp. The throttle should
|
||
|
|
// short-circuit and return null without firing the HTTP call,
|
||
|
|
// which means the test passes deterministically offline.
|
||
|
|
File.WriteAllText(
|
||
|
|
Path.Combine(_tempDir, "last-update-check.txt"),
|
||
|
|
DateTimeOffset.UtcNow.ToString("o"));
|
||
|
|
|
||
|
|
var result = await UpdateChecker.CheckIfDueAsync(TimeSpan.FromHours(24));
|
||
|
|
|
||
|
|
result.Should().BeNull("a stamp inside the cooldown window suppresses the check");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CheckIfDueAsync_ReturnsNull_WhenStampIsOldButCooldownIsLargerThanGap()
|
||
|
|
{
|
||
|
|
// Edge case: stamp 1h old, cooldown 24h → still suppressed.
|
||
|
|
File.WriteAllText(
|
||
|
|
Path.Combine(_tempDir, "last-update-check.txt"),
|
||
|
|
DateTimeOffset.UtcNow.AddHours(-1).ToString("o"));
|
||
|
|
|
||
|
|
var result = await UpdateChecker.CheckIfDueAsync(TimeSpan.FromHours(24));
|
||
|
|
|
||
|
|
result.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void LaunchCheckEnabled_RoundTrips()
|
||
|
|
{
|
||
|
|
// Default (no flag file) → enabled.
|
||
|
|
UpdateChecker.LaunchCheckEnabled.Should().BeTrue();
|
||
|
|
|
||
|
|
UpdateChecker.LaunchCheckEnabled = false;
|
||
|
|
UpdateChecker.LaunchCheckEnabled.Should().BeFalse(
|
||
|
|
"writing the opt-out flag should be visible immediately");
|
||
|
|
File.Exists(Path.Combine(_tempDir, "no-update-check.flag"))
|
||
|
|
.Should().BeTrue();
|
||
|
|
|
||
|
|
UpdateChecker.LaunchCheckEnabled = true;
|
||
|
|
UpdateChecker.LaunchCheckEnabled.Should().BeTrue();
|
||
|
|
File.Exists(Path.Combine(_tempDir, "no-update-check.flag"))
|
||
|
|
.Should().BeFalse("re-enabling should remove the opt-out flag");
|
||
|
|
}
|
||
|
|
}
|