1fc7792cd1
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.
238 lines
8.8 KiB
C#
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>());
|
|
}
|
|
}
|