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(); _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()).Returns(callInfo => { var paths = callInfo.Arg(); 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()); } [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()) .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(), Arg.Any()); } [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()) .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()) .Returns(Task.FromResult(Array.Empty())); // 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()); } [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()) .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(), Arg.Any()); } [Fact] public async Task CreateBackupAsync_WithNonExistentFile_ThrowsFileNotFoundException() { // Arrange var sourcePath = "/config/nonexistent.json"; _fileSystem.FileExists(sourcePath).Returns(false); // Act & Assert await Should.ThrowAsync( () => _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()) .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()) .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(), Arg.Any()); } }