using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using PowderCoating.Application.Configuration; using PowderCoating.Application.Interfaces; using PowderCoating.Application.Services; namespace PowderCoating.UnitTests; public class StorageMigrationServiceTests { [Fact] public async Task MigrateFilesystemToAzureAsync_ReturnsError_WhenDirectoryMissing() { var service = CreateService(); var missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var result = await service.MigrateFilesystemToAzureAsync(missingPath); Assert.Equal(0, result.Failed); Assert.Equal(0, result.Total); Assert.Contains("Media directory not found", result.Errors.Single()); Assert.True(result.HasErrors); } [Fact] public async Task MigrateFilesystemToAzureAsync_MarksUnknownPathsAsFailed() { var mediaRoot = CreateTempMediaRoot(); try { var unknownPath = Path.Combine(mediaRoot, "1", "misc"); Directory.CreateDirectory(unknownPath); await File.WriteAllTextAsync(Path.Combine(unknownPath, "file.bin"), "abc"); var service = CreateService(); var result = await service.MigrateFilesystemToAzureAsync(mediaRoot); Assert.Equal(1, result.Failed); Assert.Contains(result.Errors, e => e.Contains("Unknown file type")); Assert.Equal(MigrationFileStatus.Failed, result.Files.Single().Status); } finally { SafeDeleteDirectory(mediaRoot); } } [Fact] public async Task MigrateFilesystemToAzureAsync_SkipsExistingBlobs() { var mediaRoot = CreateTempMediaRoot(); try { var filePath = WriteMediaFile(mediaRoot, "1/profile-photos/user.jpg", "profile"); var relativePath = "1/profile-photos/user.jpg"; var blobService = new Mock(); blobService .Setup(x => x.ExistsAsync("profileimages", relativePath)) .ReturnsAsync(true); var service = CreateService(blobService); var result = await service.MigrateFilesystemToAzureAsync(mediaRoot); Assert.Equal(1, result.Skipped); Assert.Equal(0, result.Migrated); Assert.Equal(MigrationFileStatus.Skipped, result.Files.Single().Status); Assert.True(File.Exists(filePath)); } finally { SafeDeleteDirectory(mediaRoot); } } [Fact] public async Task MigrateFilesystemToAzureAsync_RecordsUploadFailures() { var mediaRoot = CreateTempMediaRoot(); try { WriteMediaFile(mediaRoot, "1/job-photos/9/photo.png", "photo"); const string relativePath = "1/job-photos/9/photo.png"; var blobService = new Mock(); blobService.Setup(x => x.ExistsAsync("jobimages", relativePath)).ReturnsAsync(false); blobService .Setup(x => x.UploadAsync("jobimages", relativePath, It.IsAny(), "image/png")) .ReturnsAsync((false, "upload failed")); var service = CreateService(blobService); var result = await service.MigrateFilesystemToAzureAsync(mediaRoot); Assert.Equal(1, result.Failed); Assert.Contains(result.Errors, e => e.Contains("upload failed")); Assert.Equal(MigrationFileStatus.Failed, result.Files.Single().Status); } finally { SafeDeleteDirectory(mediaRoot); } } [Fact] public async Task MigrateFilesystemToAzureAsync_DeletesLocalFileAfterSuccessfulMigration_WhenRequested() { var mediaRoot = CreateTempMediaRoot(); try { var fullPath = WriteMediaFile(mediaRoot, "1/company-logo.png", "logo"); const string relativePath = "1/company-logo.png"; var blobService = new Mock(); blobService.Setup(x => x.ExistsAsync("companylogos", relativePath)).ReturnsAsync(false); blobService .Setup(x => x.UploadAsync("companylogos", relativePath, It.IsAny(), "image/png")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService); var result = await service.MigrateFilesystemToAzureAsync(mediaRoot, deleteLocalAfterMigration: true); Assert.Equal(1, result.Migrated); Assert.Contains(result.Files, f => f.RelativePath == relativePath && f.Status == MigrationFileStatus.Migrated); Assert.False(File.Exists(fullPath)); } finally { SafeDeleteDirectory(mediaRoot); } } [Fact] public async Task MigrateFilesystemToAzureAsync_ContinuesAfterPerFileException() { var mediaRoot = CreateTempMediaRoot(); try { WriteMediaFile(mediaRoot, "1/profile-photos/user.jpg", "profile"); WriteMediaFile(mediaRoot, "1/equipment-manuals/manual.pdf", "manual"); var blobService = new Mock(); blobService .Setup(x => x.ExistsAsync("profileimages", "1/profile-photos/user.jpg")) .ThrowsAsync(new InvalidOperationException("broken exists")); blobService .Setup(x => x.ExistsAsync("manuals", "1/equipment-manuals/manual.pdf")) .ReturnsAsync(false); blobService .Setup(x => x.UploadAsync("manuals", "1/equipment-manuals/manual.pdf", It.IsAny(), "application/pdf")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService); var result = await service.MigrateFilesystemToAzureAsync(mediaRoot); Assert.Equal(1, result.Failed); Assert.Equal(1, result.Migrated); Assert.Contains(result.Errors, e => e.Contains("broken exists")); Assert.Equal(2, result.Total); } finally { SafeDeleteDirectory(mediaRoot); } } private static StorageMigrationService CreateService(Mock? blobService = null) { var settings = Options.Create(new StorageSettings { Containers = new StorageContainers { ProfileImages = "profileimages", JobImages = "jobimages", Manuals = "manuals", CompanyLogos = "companylogos" } }); return new StorageMigrationService( (blobService ?? new Mock()).Object, settings, Mock.Of>()); } private static string CreateTempMediaRoot() { var path = Path.Combine(Path.GetTempPath(), "pca-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } private static string WriteMediaFile(string mediaRoot, string relativePath, string content) { var fullPath = Path.Combine(mediaRoot, relativePath.Replace("/", Path.DirectorySeparatorChar.ToString())); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); File.WriteAllText(fullPath, content); return fullPath; } private static void SafeDeleteDirectory(string path) { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } }