diff --git a/src/ScadaLink.TemplateEngine/Services/TemplateFolderService.cs b/src/ScadaLink.TemplateEngine/Services/TemplateFolderService.cs new file mode 100644 index 0000000..53ee7c9 --- /dev/null +++ b/src/ScadaLink.TemplateEngine/Services/TemplateFolderService.cs @@ -0,0 +1,45 @@ +using ScadaLink.Commons.Entities.Templates; +using ScadaLink.Commons.Interfaces.Repositories; +using ScadaLink.Commons.Interfaces.Services; +using ScadaLink.Commons.Types; + +namespace ScadaLink.TemplateEngine.Services; + +public class TemplateFolderService +{ + private readonly ITemplateEngineRepository _repository; + private readonly IAuditService _auditService; + + public TemplateFolderService(ITemplateEngineRepository repository, IAuditService auditService) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); + } + + public async Task> CreateFolderAsync( + string name, int? parentFolderId, string user, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(name)) + return Result.Failure("Folder name is required."); + + if (parentFolderId.HasValue) + { + var parent = await _repository.GetFolderByIdAsync(parentFolderId.Value, cancellationToken); + if (parent == null) + return Result.Failure($"Parent folder with ID {parentFolderId.Value} not found."); + } + + var all = await _repository.GetAllFoldersAsync(cancellationToken); + if (all.Any(f => f.ParentFolderId == parentFolderId + && string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase))) + return Result.Failure($"A folder named '{name}' already exists at this level."); + + var folder = new TemplateFolder(name) { ParentFolderId = parentFolderId }; + await _repository.AddFolderAsync(folder, cancellationToken); + await _repository.SaveChangesAsync(cancellationToken); + await _auditService.LogAsync(user, "Create", "TemplateFolder", folder.Id.ToString(), name, folder, cancellationToken); + + return Result.Success(folder); + } +} diff --git a/tests/ScadaLink.TemplateEngine.Tests/Services/TemplateFolderServiceTests.cs b/tests/ScadaLink.TemplateEngine.Tests/Services/TemplateFolderServiceTests.cs new file mode 100644 index 0000000..a8c7327 --- /dev/null +++ b/tests/ScadaLink.TemplateEngine.Tests/Services/TemplateFolderServiceTests.cs @@ -0,0 +1,70 @@ +using Moq; +using ScadaLink.Commons.Entities.Templates; +using ScadaLink.Commons.Interfaces.Repositories; +using ScadaLink.Commons.Interfaces.Services; +using ScadaLink.TemplateEngine.Services; + +namespace ScadaLink.TemplateEngine.Tests.Services; + +public class TemplateFolderServiceTests +{ + private readonly Mock _repoMock = new(); + private readonly Mock _auditMock = new(); + private readonly TemplateFolderService _sut; + + public TemplateFolderServiceTests() + { + _sut = new TemplateFolderService(_repoMock.Object, _auditMock.Object); + } + + [Fact] + public async Task CreateFolder_ValidInput_ReturnsSuccess() + { + _repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny())) + .ReturnsAsync(new List()); + + var result = await _sut.CreateFolderAsync("Dev", null, "admin"); + + Assert.True(result.IsSuccess); + Assert.Equal("Dev", result.Value.Name); + Assert.Null(result.Value.ParentFolderId); + _repoMock.Verify(r => r.AddFolderAsync(It.IsAny(), It.IsAny()), Times.Once); + _repoMock.Verify(r => r.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task CreateFolder_EmptyName_ReturnsFailure() + { + var result = await _sut.CreateFolderAsync(" ", null, "admin"); + + Assert.True(result.IsFailure); + Assert.Contains("required", result.Error, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task CreateFolder_DuplicateSiblingName_CaseInsensitive_ReturnsFailure() + { + _repoMock.Setup(r => r.GetAllFoldersAsync(It.IsAny())) + .ReturnsAsync(new List + { + new("Dev") { Id = 1, ParentFolderId = null } + }); + + var result = await _sut.CreateFolderAsync("dev", null, "admin"); + + Assert.True(result.IsFailure); + Assert.Contains("already exists", result.Error); + } + + [Fact] + public async Task CreateFolder_ParentNotFound_ReturnsFailure() + { + _repoMock.Setup(r => r.GetFolderByIdAsync(99, It.IsAny())) + .ReturnsAsync((TemplateFolder?)null); + + var result = await _sut.CreateFolderAsync("Sub", 99, "admin"); + + Assert.True(result.IsFailure); + Assert.Contains("not found", result.Error); + } +}