Files
Joseph Doherty 1fc7792cd1 refactor(configmanager): rename UI project and split test projects
Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project
structure, and split the monolithic test project into Core.Tests,
Cli.Tests, and Ui.Tests to align with the source project organization.
2026-01-28 10:24:36 -05:00

238 lines
8.8 KiB
C#

using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Core.Tests.Services;
public class BackupServiceTests
{
private readonly IFileSystem _fileSystem;
private readonly BackupService _sut;
public BackupServiceTests()
{
_fileSystem = Substitute.For<IFileSystem>();
_sut = new BackupService(_fileSystem);
}
[Fact]
public async Task CreateBackupAsync_CreatesTimestampedBackup()
{
// Arrange
var sourcePath = "/config/appsettings.json";
_fileSystem.FileExists(sourcePath).Returns(true);
_fileSystem.GetDirectoryName(sourcePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(sourcePath).Returns("appsettings");
_fileSystem.Combine(Arg.Any<string[]>()).Returns(callInfo =>
{
var paths = callInfo.Arg<string[]>();
return string.Join("/", paths);
});
// Act
var backupPath = await _sut.CreateBackupAsync(sourcePath);
// Assert
backupPath.ShouldStartWith("/config/appsettings.");
backupPath.ShouldEndWith(".bak");
await _fileSystem.Received(1).CopyFileAsync(sourcePath, backupPath, Arg.Any<CancellationToken>());
}
[Fact]
public async Task CleanupOldBackupsAsync_KeepsOnlySpecifiedCount()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
var backups = Enumerable.Range(1, 15)
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
.ToArray();
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
// Mock GetFileNameWithoutExtension for each backup file
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
// Assert - should delete 5 oldest backups
await _fileSystem.Received(5).DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task GetBackupsAsync_ReturnsBackupsSortedByTimestampDescending()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Backups in random order
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak",
"/config/appsettings.2026-01-10_120000.bak",
"/config/appsettings.2026-01-20_120000.bak"
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
// Mock GetFileNameWithoutExtension for each backup file
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(3);
// Should be sorted descending by timestamp (newest first)
result[0].Path.ShouldContain("2026-01-20");
result[1].Path.ShouldContain("2026-01-15");
result[2].Path.ShouldContain("2026-01-10");
}
[Fact]
public async Task GetBackupsAsync_WithNoBackups_ReturnsEmptyList()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(Array.Empty<string>()));
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.ShouldBeEmpty();
}
[Fact]
public async Task RestoreBackupAsync_CopiesBackupToTarget()
{
// Arrange
var backupPath = "/config/appsettings.2026-01-15_120000.bak";
var targetPath = "/config/appsettings.json";
// Act
await _sut.RestoreBackupAsync(backupPath, targetPath);
// Assert
await _fileSystem.Received(1).CopyFileAsync(backupPath, targetPath, Arg.Any<CancellationToken>());
}
[Fact]
public async Task CleanupOldBackupsAsync_WithFewerThanKeepCount_DeletesNone()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Only 3 backups, but keepCount is 10
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak",
"/config/appsettings.2026-01-16_120000.bak",
"/config/appsettings.2026-01-17_120000.bak"
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
// Assert - no files should be deleted
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task CreateBackupAsync_WithNonExistentFile_ThrowsFileNotFoundException()
{
// Arrange
var sourcePath = "/config/nonexistent.json";
_fileSystem.FileExists(sourcePath).Returns(false);
// Act & Assert
await Should.ThrowAsync<FileNotFoundException>(
() => _sut.CreateBackupAsync(sourcePath));
}
[Fact]
public async Task GetBackupsAsync_SkipsFilesWithInvalidTimestampFormat()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Mix of valid and invalid backup filenames
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak", // Valid
"/config/appsettings.invalid-format.bak", // Invalid
"/config/appsettings.2026-01-16_120000.bak" // Valid
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
_fileSystem.GetFileNameWithoutExtension(backups[0]).Returns("appsettings.2026-01-15_120000");
_fileSystem.GetFileNameWithoutExtension(backups[1]).Returns("appsettings.invalid-format");
_fileSystem.GetFileNameWithoutExtension(backups[2]).Returns("appsettings.2026-01-16_120000");
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(2); // Only valid backups
}
[Fact]
public async Task CleanupOldBackupsAsync_WithExactKeepCount_DeletesNone()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Exactly 5 backups with keepCount of 5
var backups = Enumerable.Range(1, 5)
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
.ToArray();
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 5);
// Assert - no files should be deleted
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
}