diff --git a/.gitignore b/.gitignore index 766032c..5169b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ msbuild.binlog .vscode/ *.binlog *.nupkg -/Server/Content/Log +/Server/Content /Server/Data /TestResults/ diff --git a/Client.Tests/BaseTest.cs b/Client.Tests/BaseTest.cs index 4482f28..bb30e35 100644 --- a/Client.Tests/BaseTest.cs +++ b/Client.Tests/BaseTest.cs @@ -62,8 +62,8 @@ private void RegisterServices() TestContext.Services.AddLogging(); TestContext.Services.AddScoped(); - TestContext.Services.AddScoped(); TestContext.Services.AddScoped(); + TestContext.Services.AddScoped(); TestContext.Services.AddScoped(); TestContext.Services.AddScoped(); TestContext.Services.AddScoped(); @@ -156,7 +156,7 @@ protected Module CreateModuleState(int moduleId = 1, int pageId = 1, string titl PageId = pageId, Title = title, SiteId = 1, - ModuleDefinitionName = "ICTAce.FileHub.SampleModule", + ModuleDefinitionName = "ICTAce.FileHub", AllPages = false, IsDeleted = false, Pane = "Content", @@ -164,8 +164,8 @@ protected Module CreateModuleState(int moduleId = 1, int pageId = 1, string titl ContainerType = string.Empty, ModuleDefinition = new ModuleDefinition { - ModuleDefinitionName = "ICTAce.FileHub.SampleModule", - Name = "Sample Module", + ModuleDefinitionName = "ICTAce.FileHub", + Name = "FileHub", Version = "1.0.0", ServerManagerType = string.Empty, ControlTypeTemplate = string.Empty, diff --git a/Client.Tests/Mocks/MockFileService.cs b/Client.Tests/Mocks/MockFileService.cs new file mode 100644 index 0000000..e210929 --- /dev/null +++ b/Client.Tests/Mocks/MockFileService.cs @@ -0,0 +1,172 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Client.Tests.Mocks; + +public class MockFileService : Services.IFileService +{ + private readonly List _files = []; + private int _nextId = 1; + + public MockFileService() + { + _files.Add(new GetFileDto + { + Id = 1, + ModuleId = 1, + Name = "Test File 1", + FileName = "test-file-1.pdf", + ImageName = "test-image-1.png", + Description = "Test file description 1", + FileSize = "1.5 MB", + Downloads = 10, + CategoryIds = [1], + CreatedBy = "Test User", + CreatedOn = DateTime.Now.AddDays(-10), + ModifiedBy = "Test User", + ModifiedOn = DateTime.Now.AddDays(-5) + }); + + _files.Add(new GetFileDto + { + Id = 2, + ModuleId = 1, + Name = "Test File 2", + FileName = "test-file-2.docx", + ImageName = "test-image-2.jpg", + Description = "Test file description 2", + FileSize = "2.3 MB", + Downloads = 25, + CategoryIds = [1, 2], + CreatedBy = "Test User", + CreatedOn = DateTime.Now.AddDays(-8), + ModifiedBy = "Test User", + ModifiedOn = DateTime.Now.AddDays(-3) + }); + + _nextId = 3; + } + + public Task GetAsync(int id, int moduleId) + { + var file = _files.FirstOrDefault(f => f.Id == id && f.ModuleId == moduleId); + if (file == null) + { + throw new InvalidOperationException($"File with Id {id} and ModuleId {moduleId} not found"); + } + return Task.FromResult(file); + } + + public Task> ListAsync(int moduleId, int pageNumber = 1, int pageSize = 10) + { + var items = _files + .Where(f => f.ModuleId == moduleId) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(f => new ListFileDto + { + Id = f.Id, + Name = f.Name, + FileName = f.FileName, + ImageName = f.ImageName, + Description = f.Description, + FileSize = f.FileSize, + Downloads = f.Downloads, + CreatedOn = f.CreatedOn + }) + .ToList(); + + var totalCount = _files.Count(f => f.ModuleId == moduleId); + + var pagedResult = new PagedResult + { + Items = items, + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize + }; + + return Task.FromResult(pagedResult); + } + + public Task CreateAsync(int moduleId, CreateAndUpdateFileDto dto) + { + var newFile = new GetFileDto + { + Id = _nextId++, + ModuleId = moduleId, + Name = dto.Name, + FileName = dto.FileName, + ImageName = dto.ImageName, + Description = dto.Description, + FileSize = dto.FileSize, + Downloads = dto.Downloads, + CategoryIds = dto.CategoryIds ?? [], + CreatedBy = "Test User", + CreatedOn = DateTime.Now, + ModifiedBy = "Test User", + ModifiedOn = DateTime.Now + }; + + _files.Add(newFile); + return Task.FromResult(newFile.Id); + } + + public Task UpdateAsync(int id, int moduleId, CreateAndUpdateFileDto dto) + { + var file = _files.FirstOrDefault(f => f.Id == id && f.ModuleId == moduleId); + if (file == null) + { + throw new InvalidOperationException($"File with Id {id} and ModuleId {moduleId} not found"); + } + + file.Name = dto.Name; + file.FileName = dto.FileName; + file.ImageName = dto.ImageName; + file.Description = dto.Description; + file.FileSize = dto.FileSize; + file.Downloads = dto.Downloads; + file.CategoryIds = dto.CategoryIds ?? []; + file.ModifiedBy = "Test User"; + file.ModifiedOn = DateTime.Now; + + return Task.FromResult(file.Id); + } + + public Task DeleteAsync(int id, int moduleId) + { + var file = _files.FirstOrDefault(f => f.Id == id && f.ModuleId == moduleId); + if (file != null) + { + _files.Remove(file); + } + return Task.CompletedTask; + } + + public Task UploadFileAsync(int moduleId, Stream fileStream, string fileName) + { + // Simulate file upload by generating a unique filename + var uniqueFileName = $"{Guid.NewGuid()}_{fileName}"; + return Task.FromResult(uniqueFileName); + } + + public void ClearData() + { + _files.Clear(); + _nextId = 1; + } + + public void AddTestData(GetFileDto file) + { + _files.Add(file); + } + + public int GetFileCount() + { + return _files.Count; + } + + public List GetAllFiles() + { + return _files; + } +} diff --git a/Client.Tests/Mocks/MockMyModuleService.cs b/Client.Tests/Mocks/MockMyModuleService.cs deleted file mode 100644 index da68d52..0000000 --- a/Client.Tests/Mocks/MockMyModuleService.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Client.Tests.Mocks; - -public class MockSampleModuleService : ISampleModuleService -{ - private readonly List _modules = new(); - private int _nextId = 1; - - public MockSampleModuleService() - { - _modules.Add(new GetSampleModuleDto - { - Id = 1, - ModuleId = 1, - Name = "Test Module 1", - CreatedBy = "Test User", - CreatedOn = DateTime.Now.AddDays(-10), - ModifiedBy = "Test User", - ModifiedOn = DateTime.Now.AddDays(-5) - }); - - _modules.Add(new GetSampleModuleDto - { - Id = 2, - ModuleId = 1, - Name = "Test Module 2", - CreatedBy = "Test User", - CreatedOn = DateTime.Now.AddDays(-8), - ModifiedBy = "Test User", - ModifiedOn = DateTime.Now.AddDays(-3) - }); - - _nextId = 3; - } - - public Task GetAsync(int id, int moduleId) - { - var module = _modules.FirstOrDefault(m => m.Id == id && m.ModuleId == moduleId); - if (module == null) - { - throw new InvalidOperationException($"Module with Id {id} and ModuleId {moduleId} not found"); - } - return Task.FromResult(module); - } - - public Task> ListAsync(int moduleId, int pageNumber = 1, int pageSize = 10) - { - var items = _modules - .Where(m => m.ModuleId == moduleId) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize) - .Select(m => new ListSampleModuleDto - { - Id = m.Id, - Name = m.Name - }) - .ToList(); - - var totalCount = _modules.Count(m => m.ModuleId == moduleId); - - var pagedResult = new PagedResult - { - Items = items, - TotalCount = totalCount, - PageNumber = pageNumber, - PageSize = pageSize - }; - - return Task.FromResult(pagedResult); - } - - public Task CreateAsync(int moduleId, CreateAndUpdateSampleModuleDto dto) - { - var newModule = new GetSampleModuleDto - { - Id = _nextId++, - ModuleId = moduleId, - Name = dto.Name, - CreatedBy = "Test User", - CreatedOn = DateTime.Now, - ModifiedBy = "Test User", - ModifiedOn = DateTime.Now - }; - - _modules.Add(newModule); - return Task.FromResult(newModule.Id); - } - - public Task UpdateAsync(int id, int moduleId, CreateAndUpdateSampleModuleDto dto) - { - var module = _modules.FirstOrDefault(m => m.Id == id && m.ModuleId == moduleId); - if (module == null) - { - throw new InvalidOperationException($"Module with Id {id} and ModuleId {moduleId} not found"); - } - - module.Name = dto.Name; - module.ModifiedBy = "Test User"; - module.ModifiedOn = DateTime.Now; - - return Task.FromResult(module.Id); - } - - public Task DeleteAsync(int id, int moduleId) - { - var module = _modules.FirstOrDefault(m => m.Id == id && m.ModuleId == moduleId); - if (module != null) - { - _modules.Remove(module); - } - return Task.CompletedTask; - } - - public void ClearData() - { - _modules.Clear(); - _nextId = 1; - } - - public void AddTestData(GetSampleModuleDto module) - { - _modules.Add(module); - } - - public int GetModuleCount() - { - return _modules.Count; - } -} diff --git a/Client.Tests/Mocks/MockOqtaneServices.cs b/Client.Tests/Mocks/MockOqtaneServices.cs index fc407ae..f81a131 100644 --- a/Client.Tests/Mocks/MockOqtaneServices.cs +++ b/Client.Tests/Mocks/MockOqtaneServices.cs @@ -101,8 +101,8 @@ public Task> GetModuleDefinitionsAsync(int siteId) { new ModuleDefinition { - ModuleDefinitionName = "ICTAce.FileHub.SampleModule", - Name = "Sample Module", + ModuleDefinitionName = "ICTAce.FileHub", + Name = "FileHub", Version = "1.0.0" } }); @@ -113,8 +113,8 @@ public Task GetModuleDefinitionAsync(int moduleDefinitionId, i return Task.FromResult(new ModuleDefinition { ModuleDefinitionId = moduleDefinitionId, - ModuleDefinitionName = "ICTAce.FileHub.SampleModule", - Name = "Sample Module", + ModuleDefinitionName = "ICTAce.FileHub", + Name = "FileHub", Version = "1.0.0" }); } @@ -124,7 +124,7 @@ public Task GetModuleDefinitionAsync(string moduleDefinitionNa return Task.FromResult(new ModuleDefinition { ModuleDefinitionName = moduleDefinitionName, - Name = "Sample Module", + Name = "FileHub", Version = "1.0.0" }); } diff --git a/Client.Tests/Modules/FileHub/EditTests.cs b/Client.Tests/Modules/FileHub/EditTests.cs new file mode 100644 index 0000000..d42a335 --- /dev/null +++ b/Client.Tests/Modules/FileHub/EditTests.cs @@ -0,0 +1,588 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Client.Tests.Modules.FileHub; + +public class EditTests : BaseTest +{ + private readonly MockNavigationManager? _mockNavigationManager; + private readonly MockFileService? _mockFileService; + private readonly MockCategoryService? _mockCategoryService; + + public EditTests() + { + _mockNavigationManager = TestContext.Services.GetRequiredService() as MockNavigationManager; + _mockFileService = TestContext.Services.GetRequiredService() as MockFileService; + _mockCategoryService = TestContext.Services.GetRequiredService() as MockCategoryService; + TestContext.JSInterop.Setup("Oqtane.Interop.formValid", _ => true).SetResult(true); + } + + #region Service Dependency Tests + + [Test] + public async Task EditComponent_ServiceDependencies_AreConfigured() + { + await Assert.That(_mockFileService).IsNotNull(); + await Assert.That(_mockCategoryService).IsNotNull(); + await Assert.That(_mockNavigationManager).IsNotNull(); + + var logService = TestContext.Services.GetService(); + await Assert.That(logService).IsNotNull(); + } + + #endregion + + #region Service Layer Tests - CRUD Operations + + [Test] + public async Task ServiceLayer_CreateAsync_AddsNewFile() + { + var initialCount = _mockFileService!.GetFileCount(); + + var dto = new CreateAndUpdateFileDto + { + Name = "New Test File", + FileName = "new-test-file.pdf", + ImageName = "new-test-image.png", + Description = "New test file description", + FileSize = "3.5 MB", + Downloads = 0, + CategoryIds = [1] + }; + + var newId = await _mockFileService.CreateAsync(1, dto); + + await Assert.That(newId).IsGreaterThan(0); + await Assert.That(_mockFileService.GetFileCount()).IsEqualTo(initialCount + 1); + + var created = await _mockFileService.GetAsync(newId, 1); + await Assert.That(created.Name).IsEqualTo("New Test File"); + await Assert.That(created.FileName).IsEqualTo("new-test-file.pdf"); + await Assert.That(created.ImageName).IsEqualTo("new-test-image.png"); + await Assert.That(created.Description).IsEqualTo("New test file description"); + await Assert.That(created.FileSize).IsEqualTo("3.5 MB"); + await Assert.That(created.CategoryIds.Count).IsEqualTo(1); + await Assert.That(created.CategoryIds[0]).IsEqualTo(1); + } + + [Test] + public async Task ServiceLayer_CreateAsync_WithMultipleCategories() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Multi-Category File", + FileName = "multi-cat-file.pdf", + ImageName = "multi-cat-image.png", + Description = "File with multiple categories", + FileSize = "1.0 MB", + Downloads = 0, + CategoryIds = [1, 2] + }; + + var newId = await _mockFileService!.CreateAsync(1, dto); + var created = await _mockFileService.GetAsync(newId, 1); + + await Assert.That(created.CategoryIds.Count).IsEqualTo(2); + await Assert.That(created.CategoryIds.Contains(1)).IsTrue(); + await Assert.That(created.CategoryIds.Contains(2)).IsTrue(); + } + + [Test] + public async Task ServiceLayer_CreateAsync_WithNoCategoriesWorks() + { + var dto = new CreateAndUpdateFileDto + { + Name = "No Category File", + FileName = "no-cat-file.pdf", + ImageName = "no-cat-image.png", + Description = "File without categories", + FileSize = "2.0 MB", + Downloads = 0, + CategoryIds = [] + }; + + var newId = await _mockFileService!.CreateAsync(1, dto); + var created = await _mockFileService.GetAsync(newId, 1); + + await Assert.That(created.CategoryIds.Count).IsEqualTo(0); + } + + [Test] + public async Task ServiceLayer_UpdateAsync_ModifiesExistingFile() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Updated File Name", + FileName = "updated-file.pdf", + ImageName = "updated-image.png", + Description = "Updated description", + FileSize = "5.0 MB", + Downloads = 0, + CategoryIds = [2] + }; + + await _mockFileService!.UpdateAsync(1, 1, dto); + + var updated = await _mockFileService.GetAsync(1, 1); + await Assert.That(updated.Name).IsEqualTo("Updated File Name"); + await Assert.That(updated.FileName).IsEqualTo("updated-file.pdf"); + await Assert.That(updated.ImageName).IsEqualTo("updated-image.png"); + await Assert.That(updated.Description).IsEqualTo("Updated description"); + await Assert.That(updated.FileSize).IsEqualTo("5.0 MB"); + await Assert.That(updated.Id).IsEqualTo(1); + } + + [Test] + public async Task ServiceLayer_GetAsync_ReturnsCorrectFile() + { + var file1 = await _mockFileService!.GetAsync(1, 1); + var file2 = await _mockFileService.GetAsync(2, 1); + + await Assert.That(file1.Id).IsEqualTo(1); + await Assert.That(file1.Name).IsEqualTo("Test File 1"); + await Assert.That(file1.FileName).IsEqualTo("test-file-1.pdf"); + await Assert.That(file1.Downloads).IsEqualTo(10); + + await Assert.That(file2.Id).IsEqualTo(2); + await Assert.That(file2.Name).IsEqualTo("Test File 2"); + await Assert.That(file2.FileName).IsEqualTo("test-file-2.docx"); + await Assert.That(file2.Downloads).IsEqualTo(25); + } + + [Test] + public async Task ServiceLayer_DeleteAsync_RemovesFile() + { + var initialCount = _mockFileService!.GetFileCount(); + + await _mockFileService.DeleteAsync(2, 1); + + await Assert.That(_mockFileService.GetFileCount()).IsEqualTo(initialCount - 1); + } + + #endregion + + #region Service Layer Tests - File Upload + + [Test] + public async Task ServiceLayer_UploadFileAsync_ReturnsUniqueFileName() + { + using var stream = new MemoryStream(); + var fileName = "test-upload.pdf"; + + var result = await _mockFileService!.UploadFileAsync(1, stream, fileName); + + await Assert.That(result).IsNotNull(); + await Assert.That(result).Contains(fileName); + await Assert.That(result.Length).IsGreaterThan(fileName.Length); + } + + [Test] + public async Task ServiceLayer_UploadFileAsync_GeneratesDifferentNames() + { + using var stream1 = new MemoryStream(); + using var stream2 = new MemoryStream(); + var fileName = "test-upload.pdf"; + + var result1 = await _mockFileService!.UploadFileAsync(1, stream1, fileName); + var result2 = await _mockFileService.UploadFileAsync(1, stream2, fileName); + + await Assert.That(result1).IsNotEqualTo(result2); + } + + #endregion + + #region State Management Tests + + [Test] + public async Task PageState_AddMode_IsConfigured() + { + var pageState = CreatePageState("Add"); + + await Assert.That(pageState.Action).IsEqualTo("Add"); + await Assert.That(pageState.QueryString).IsNotNull(); + await Assert.That(pageState.QueryString.Count).IsEqualTo(0); + } + + [Test] + public async Task PageState_EditMode_IsConfigured() + { + var queryString = new Dictionary + { + { "id", "1" } + }; + + var pageState = CreatePageState("Edit", queryString); + + await Assert.That(pageState.Action).IsEqualTo("Edit"); + await Assert.That(pageState.QueryString).IsNotNull(); + await Assert.That(pageState.QueryString.ContainsKey("id")).IsTrue(); + await Assert.That(pageState.QueryString["id"]).IsEqualTo("1"); + } + + #endregion + + #region Form Validation Tests + + [Test] + public async Task FormValidation_ValidData_Passes() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid File Name", + FileName = "valid-file.pdf", + ImageName = "valid-image.png", + Description = "Valid description", + FileSize = "1.5 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsTrue(); + await Assert.That(validationResults.Count).IsEqualTo(0); + } + + [Test] + public async Task FormValidation_EmptyName_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = string.Empty, + FileName = "file.pdf", + ImageName = "image.png", + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Count).IsGreaterThan(0); + await Assert.That(validationResults.Any(v => v.MemberNames.Contains("Name"))).IsTrue(); + } + + [Test] + public async Task FormValidation_NameTooLong_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = new string('A', 101), + FileName = "file.pdf", + ImageName = "image.png", + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("100") == true)).IsTrue(); + } + + [Test] + public async Task FormValidation_EmptyFileName_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = string.Empty, + ImageName = "image.png", + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.MemberNames.Contains("FileName"))).IsTrue(); + } + + [Test] + public async Task FormValidation_FileNameTooLong_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = new string('A', 256), + ImageName = "image.png", + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("255") == true)).IsTrue(); + } + + [Test] + public async Task FormValidation_EmptyImageName_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = string.Empty, + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.MemberNames.Contains("ImageName"))).IsTrue(); + } + + [Test] + public async Task FormValidation_ImageNameTooLong_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = new string('A', 256), + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("255") == true)).IsTrue(); + } + + [Test] + public async Task FormValidation_DescriptionTooLong_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = "image.png", + Description = new string('A', 1001), + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("1000") == true)).IsTrue(); + } + + [Test] + public async Task FormValidation_EmptyFileSize_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = "image.png", + FileSize = string.Empty + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.MemberNames.Contains("FileSize"))).IsTrue(); + } + + [Test] + public async Task FormValidation_FileSizeTooLong_Fails() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = "image.png", + FileSize = new string('1', 13) + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsFalse(); + await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("12") == true)).IsTrue(); + } + + [Test] + public async Task FormValidation_NullDescription_Passes() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Valid Name", + FileName = "file.pdf", + ImageName = "image.png", + Description = null, + FileSize = "1.0 MB" + }; + + var validationResults = new List(); + var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); + var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject( + dto, context, validationResults, validateAllProperties: true); + + await Assert.That(isValid).IsTrue(); + await Assert.That(validationResults.Count).IsEqualTo(0); + } + + #endregion + + #region Navigation Tests + + [Test] + public async Task NavigationManager_Reset_ClearsHistory() + { + _mockNavigationManager!.Reset(); + + await Assert.That(_mockNavigationManager.Uri).IsEqualTo("https://localhost:5001/"); + await Assert.That(_mockNavigationManager.BaseUri).IsEqualTo("https://localhost:5001/"); + } + + #endregion + + #region Permission Tests + + [Test] + public async Task ModuleState_ForEditComponent_HasRequiredProperties() + { + var moduleState = CreateModuleState(1, 1, "Test Module"); + + await Assert.That(moduleState.ModuleId).IsEqualTo(1); + await Assert.That(moduleState.PageId).IsEqualTo(1); + await Assert.That(moduleState.ModuleDefinition).IsNotNull(); + await Assert.That(moduleState.PermissionList).IsNotNull(); + await Assert.That(moduleState.PermissionList.Any(p => p.PermissionName == "Edit")).IsTrue(); + } + + #endregion + + #region Mock Service Helper Tests + + [Test] + public async Task MockService_HasTestData() + { + var count = _mockFileService!.GetFileCount(); + await Assert.That(count).IsGreaterThan(0); + await Assert.That(count).IsEqualTo(2); + } + + [Test] + public async Task MockService_GetAllFiles_ReturnsAllData() + { + var files = _mockFileService!.GetAllFiles(); + + await Assert.That(files).IsNotNull(); + await Assert.That(files.Count).IsEqualTo(2); + await Assert.That(files.Any(f => f.CategoryIds.Count > 0)).IsTrue(); + } + + [Test] + public async Task MockService_ClearData_RemovesAllFiles() + { + _mockFileService!.ClearData(); + + await Assert.That(_mockFileService.GetFileCount()).IsEqualTo(0); + } + + [Test] + public async Task MockService_AddTestData_IncreasesCount() + { + var initialCount = _mockFileService!.GetFileCount(); + + _mockFileService.AddTestData(new GetFileDto + { + Id = 99, + ModuleId = 1, + Name = "Manually Added File", + FileName = "manual-file.pdf", + ImageName = "manual-image.png", + Description = "Manually added test file", + FileSize = "5.5 MB", + Downloads = 0, + CategoryIds = [], + CreatedBy = "Test", + CreatedOn = DateTime.Now, + ModifiedBy = "Test", + ModifiedOn = DateTime.Now + }); + + await Assert.That(_mockFileService.GetFileCount()).IsEqualTo(initialCount + 1); + } + + #endregion + + #region Error Handling Tests + + [Test] + public async Task ServiceLayer_GetAsync_ThrowsForNonExistentFile() + { + await Assert.That(async () => await _mockFileService!.GetAsync(999, 1).ConfigureAwait(false)) + .ThrowsException(); + } + + [Test] + public async Task ServiceLayer_UpdateAsync_ThrowsForNonExistentFile() + { + var dto = new CreateAndUpdateFileDto + { + Name = "Updated Name", + FileName = "updated.pdf", + ImageName = "updated.png", + FileSize = "1.0 MB" + }; + + await Assert.That(async () => await _mockFileService!.UpdateAsync(999, 1, dto).ConfigureAwait(false)) + .ThrowsException(); + } + + #endregion + + #region List and Pagination Tests + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsFiles() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10).ConfigureAwait(false); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Items).IsNotNull(); + await Assert.That(result.Items.Count).IsGreaterThan(0); + await Assert.That(result.TotalCount).IsEqualTo(2); + } + + [Test] + public async Task ServiceLayer_ListAsync_SupportsPagination() + { + var page1 = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 1).ConfigureAwait(false); + var page2 = await _mockFileService.ListAsync(1, pageNumber: 2, pageSize: 1).ConfigureAwait(false); + + await Assert.That(page1.Items.Count).IsEqualTo(1); + await Assert.That(page1.PageNumber).IsEqualTo(1); + await Assert.That(page2.Items.Count).IsEqualTo(1); + await Assert.That(page2.PageNumber).IsEqualTo(2); + await Assert.That(page1.TotalCount).IsEqualTo(2); + } + + #endregion +} diff --git a/Client.Tests/Modules/FileHub/IndexTests.cs b/Client.Tests/Modules/FileHub/IndexTests.cs new file mode 100644 index 0000000..5656a76 --- /dev/null +++ b/Client.Tests/Modules/FileHub/IndexTests.cs @@ -0,0 +1,381 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Client.Tests.Modules.FileHub; + +public class IndexTests : BaseTest +{ + private readonly MockNavigationManager? _mockNavigationManager; + private readonly MockFileService? _mockFileService; + + public IndexTests() + { + _mockNavigationManager = TestContext.Services.GetRequiredService() as MockNavigationManager; + _mockFileService = TestContext.Services.GetRequiredService() as MockFileService; + } + + #region Service Dependency Tests + + [Test] + public async Task IndexComponent_ServiceDependencies_CanBeResolved() + { + await Assert.That(_mockFileService).IsNotNull(); + await Assert.That(_mockNavigationManager).IsNotNull(); + + var logService = TestContext.Services.GetService(); + await Assert.That(logService).IsNotNull(); + } + + #endregion + + #region Service Layer Tests + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsFiles() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Items).IsNotNull(); + await Assert.That(result.Items.Count).IsEqualTo(2); + await Assert.That(result.TotalCount).IsEqualTo(2); + } + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsCorrectFileData() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + var firstFile = result.Items.First(); + await Assert.That(firstFile.Id).IsEqualTo(1); + await Assert.That(firstFile.Name).IsEqualTo("Test File 1"); + await Assert.That(firstFile.FileName).IsEqualTo("test-file-1.pdf"); + await Assert.That(firstFile.ImageName).IsEqualTo("test-image-1.png"); + await Assert.That(firstFile.FileSize).IsEqualTo("1.5 MB"); + await Assert.That(firstFile.Downloads).IsEqualTo(10); + } + + [Test] + public async Task ServiceLayer_ListAsync_SupportsPagination() + { + var page1 = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 1); + var page2 = await _mockFileService.ListAsync(1, pageNumber: 2, pageSize: 1); + + await Assert.That(page1.Items.Count).IsEqualTo(1); + await Assert.That(page1.PageNumber).IsEqualTo(1); + await Assert.That(page2.Items.Count).IsEqualTo(1); + await Assert.That(page2.PageNumber).IsEqualTo(2); + await Assert.That(page1.TotalCount).IsEqualTo(2); + await Assert.That(page2.TotalCount).IsEqualTo(2); + } + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsEmptyWhenNoFiles() + { + _mockFileService!.ClearData(); + + var result = await _mockFileService.ListAsync(1, pageNumber: 1, pageSize: 10); + + await Assert.That(result).IsNotNull(); + await Assert.That(result.Items).IsNotNull(); + await Assert.That(result.Items.Count).IsEqualTo(0); + await Assert.That(result.TotalCount).IsEqualTo(0); + } + + [Test] + public async Task ServiceLayer_DeleteAsync_RemovesFile() + { + var initialCount = _mockFileService!.GetFileCount(); + + await _mockFileService.DeleteAsync(1, 1); + + await Assert.That(_mockFileService.GetFileCount()).IsEqualTo(initialCount - 1); + + var result = await _mockFileService.ListAsync(1, pageNumber: 1, pageSize: 10); + await Assert.That(result.Items.Any(f => f.Id == 1)).IsFalse(); + } + + #endregion + + #region State Management Tests + + [Test] + public async Task PageState_ForIndexComponent_IsConfigured() + { + var pageState = CreatePageState("Index"); + + await Assert.That(pageState.Action).IsEqualTo("Index"); + await Assert.That(pageState.QueryString).IsNotNull(); + await Assert.That(pageState.Page).IsNotNull(); + await Assert.That(pageState.Alias).IsNotNull(); + await Assert.That(pageState.Site).IsNotNull(); + } + + [Test] + public async Task ModuleState_ForIndexComponent_HasRequiredProperties() + { + var moduleState = CreateModuleState(1, 1, "Test Module"); + + await Assert.That(moduleState.ModuleId).IsEqualTo(1); + await Assert.That(moduleState.PageId).IsEqualTo(1); + await Assert.That(moduleState.ModuleDefinition).IsNotNull(); + await Assert.That(moduleState.PermissionList).IsNotNull(); + } + + #endregion + + #region Navigation Tests + + [Test] + public async Task NavigationManager_Reset_ClearsHistory() + { + _mockNavigationManager!.Reset(); + + await Assert.That(_mockNavigationManager.Uri).IsEqualTo("https://localhost:5001/"); + await Assert.That(_mockNavigationManager.BaseUri).IsEqualTo("https://localhost:5001/"); + } + + #endregion + + #region Mock Service Tests + + [Test] + public async Task MockService_HasTestData() + { + var count = _mockFileService!.GetFileCount(); + await Assert.That(count).IsGreaterThan(0); + await Assert.That(count).IsEqualTo(2); + } + + [Test] + public async Task MockService_GetAllFiles_ReturnsCorrectCount() + { + var files = _mockFileService!.GetAllFiles(); + + await Assert.That(files).IsNotNull(); + await Assert.That(files.Count).IsEqualTo(2); + } + + [Test] + public async Task MockService_SupportsMultipleModules() + { + // Add a file for a different module + _mockFileService!.AddTestData(new GetFileDto + { + Id = 100, + ModuleId = 2, + Name = "Module 2 File", + FileName = "module2-file.pdf", + ImageName = "module2-image.png", + Description = "File for module 2", + FileSize = "1.0 MB", + Downloads = 0, + CategoryIds = [], + CreatedBy = "Test", + CreatedOn = DateTime.Now, + ModifiedBy = "Test", + ModifiedOn = DateTime.Now + }); + + var module1Files = await _mockFileService.ListAsync(1, pageNumber: 1, pageSize: 10); + var module2Files = await _mockFileService.ListAsync(2, pageNumber: 1, pageSize: 10); + + await Assert.That(module1Files.TotalCount).IsEqualTo(2); + await Assert.That(module2Files.TotalCount).IsEqualTo(1); + } + + #endregion + + #region File Properties Tests + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsFileWithAllProperties() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var file = result.Items.First(); + + await Assert.That(file.Id).IsGreaterThan(0); + await Assert.That(file.Name).IsNotNull(); + await Assert.That(file.FileName).IsNotNull(); + await Assert.That(file.ImageName).IsNotNull(); + await Assert.That(file.FileSize).IsNotNull(); + await Assert.That(file.Downloads).IsGreaterThanOrEqualTo(0); + await Assert.That(file.CreatedOn).IsGreaterThan(DateTime.MinValue); + } + + [Test] + public async Task ServiceLayer_ListAsync_ReturnsFilesInCorrectOrder() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var files = result.Items.ToList(); + + await Assert.That(files.Count).IsEqualTo(2); + await Assert.That(files[0].Id).IsEqualTo(1); + await Assert.That(files[1].Id).IsEqualTo(2); + } + + #endregion + + #region Download Counter Tests + + [Test] + public async Task FileDto_HasDownloadsProperty() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var file = result.Items.First(); + + await Assert.That(file.Downloads).IsGreaterThanOrEqualTo(0); + } + + [Test] + public async Task FileDto_DownloadsCanBeTracked() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var file1 = result.Items.First(f => f.Id == 1); + var file2 = result.Items.First(f => f.Id == 2); + + await Assert.That(file1.Downloads).IsEqualTo(10); + await Assert.That(file2.Downloads).IsEqualTo(25); + } + + #endregion + + #region Description Tests + + [Test] + public async Task FileDto_CanHaveNullDescription() + { + _mockFileService!.AddTestData(new GetFileDto + { + Id = 50, + ModuleId = 1, + Name = "No Description File", + FileName = "no-desc.pdf", + ImageName = "no-desc-image.png", + Description = null, + FileSize = "1.0 MB", + Downloads = 0, + CategoryIds = [], + CreatedBy = "Test", + CreatedOn = DateTime.Now, + ModifiedBy = "Test", + ModifiedOn = DateTime.Now + }); + + var result = await _mockFileService.ListAsync(1, pageNumber: 1, pageSize: 10); + var fileWithoutDesc = result.Items.FirstOrDefault(f => f.Id == 50); + + await Assert.That(fileWithoutDesc).IsNotNull(); + await Assert.That(fileWithoutDesc!.Description).IsNull(); + } + + [Test] + public async Task FileDto_CanHaveDescription() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var fileWithDesc = result.Items.First(f => f.Id == 1); + + await Assert.That(fileWithDesc.Description).IsNotNull(); + await Assert.That(fileWithDesc.Description).IsEqualTo("Test file description 1"); + } + + #endregion + + #region Pagination Edge Cases + + [Test] + public async Task ServiceLayer_ListAsync_HandlesLargePageSize() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 1000); + + await Assert.That(result.Items.Count).IsEqualTo(2); + await Assert.That(result.TotalCount).IsEqualTo(2); + } + + [Test] + public async Task ServiceLayer_ListAsync_HandlesPageBeyondTotalPages() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 10, pageSize: 10); + + await Assert.That(result.Items.Count).IsEqualTo(0); + await Assert.That(result.TotalCount).IsEqualTo(2); + await Assert.That(result.PageNumber).IsEqualTo(10); + } + + #endregion + + #region File Name Tests + + [Test] + public async Task FileDto_HasUniqueFileNames() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var fileNames = result.Items.Select(f => f.FileName).ToList(); + + await Assert.That(fileNames.Distinct().Count()).IsEqualTo(fileNames.Count); + } + + [Test] + public async Task FileDto_HasValidFileExtensions() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + foreach (var file in result.Items) + { + await Assert.That(file.FileName).Contains("."); + } + } + + #endregion + + #region Image Name Tests + + [Test] + public async Task FileDto_HasImageNames() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + foreach (var file in result.Items) + { + await Assert.That(file.ImageName).IsNotNull(); + await Assert.That(file.ImageName.Length).IsGreaterThan(0); + } + } + + [Test] + public async Task FileDto_ImageNamesAreValid() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + foreach (var file in result.Items) + { + await Assert.That(file.ImageName).Contains("."); + } + } + + #endregion + + #region File Size Tests + + [Test] + public async Task FileDto_HasFileSizes() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + + foreach (var file in result.Items) + { + await Assert.That(file.FileSize).IsNotNull(); + await Assert.That(file.FileSize.Length).IsGreaterThan(0); + } + } + + [Test] + public async Task FileDto_FileSizesAreFormatted() + { + var result = await _mockFileService!.ListAsync(1, pageNumber: 1, pageSize: 10); + var file1 = result.Items.First(f => f.Id == 1); + + await Assert.That(file1.FileSize).Contains("MB"); + } + + #endregion +} diff --git a/Client.Tests/Modules/SampleModule/EditTests.cs b/Client.Tests/Modules/SampleModule/EditTests.cs deleted file mode 100644 index c0694ad..0000000 --- a/Client.Tests/Modules/SampleModule/EditTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Client.Tests.Modules.SampleModule; - -public class EditTests : BaseTest -{ - private readonly MockNavigationManager? _mockNavigationManager; - private readonly MockSampleModuleService? _mockSampleModuleService; - - public EditTests() - { - _mockNavigationManager = TestContext.Services.GetRequiredService() as MockNavigationManager; - _mockSampleModuleService = TestContext.Services.GetRequiredService() as MockSampleModuleService; - TestContext.JSInterop.Setup("Oqtane.Interop.formValid", _ => true).SetResult(true); - } - - [Test] - public async Task EditComponent_ServiceDependencies_AreConfigured() - { - await Assert.That(_mockSampleModuleService).IsNotNull(); - await Assert.That(_mockNavigationManager).IsNotNull(); - - var logService = TestContext.Services.GetService(); - await Assert.That(logService).IsNotNull(); - } - - [Test] - public async Task ServiceLayer_CreateAsync_AddsNewModule() - { - var initialCount = _mockSampleModuleService!.GetModuleCount(); - - var dto = new CreateAndUpdateSampleModuleDto - { - Name = "New Test Module" - }; - - var newId = await _mockSampleModuleService.CreateAsync(1, dto); - - await Assert.That(newId).IsGreaterThan(0); - await Assert.That(_mockSampleModuleService.GetModuleCount()).IsEqualTo(initialCount + 1); - - var created = await _mockSampleModuleService.GetAsync(newId, 1); - await Assert.That(created.Name).IsEqualTo("New Test Module"); - } - - [Test] - public async Task ServiceLayer_UpdateAsync_ModifiesExistingModule() - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = "Updated Module Name" - }; - - await _mockSampleModuleService!.UpdateAsync(1, 1, dto); - - var updated = await _mockSampleModuleService.GetAsync(1, 1); - await Assert.That(updated.Name).IsEqualTo("Updated Module Name"); - await Assert.That(updated.Id).IsEqualTo(1); - } - - [Test] - public async Task ServiceLayer_GetAsync_ReturnsCorrectModule() - { - var module1 = await _mockSampleModuleService!.GetAsync(1, 1); - var module2 = await _mockSampleModuleService.GetAsync(2, 1); - - await Assert.That(module1.Id).IsEqualTo(1); - await Assert.That(module1.Name).IsEqualTo("Test Module 1"); - await Assert.That(module2.Id).IsEqualTo(2); - await Assert.That(module2.Name).IsEqualTo("Test Module 2"); - } - - [Test] - public async Task PageState_AddMode_IsConfigured() - { - var pageState = CreatePageState("Add"); - - await Assert.That(pageState.Action).IsEqualTo("Add"); - await Assert.That(pageState.QueryString).IsNotNull(); - await Assert.That(pageState.QueryString.Count).IsEqualTo(0); - } - - [Test] - public async Task PageState_EditMode_IsConfigured() - { - var queryString = new Dictionary - { - { "id", "1" } - }; - - var pageState = CreatePageState("Edit", queryString); - - await Assert.That(pageState.Action).IsEqualTo("Edit"); - await Assert.That(pageState.QueryString).IsNotNull(); - await Assert.That(pageState.QueryString.ContainsKey("id")).IsTrue(); - await Assert.That(pageState.QueryString["id"]).IsEqualTo("1"); - } - - [Test] - public async Task NavigationManager_Reset_ClearsHistory() - { - _mockNavigationManager!.Reset(); - - await Assert.That(_mockNavigationManager.Uri).IsEqualTo("https://localhost:5001/"); - await Assert.That(_mockNavigationManager.BaseUri).IsEqualTo("https://localhost:5001/"); - } - - [Test] - public async Task ModuleState_ForEditComponent_HasRequiredProperties() - { - var moduleState = CreateModuleState(1, 1, "Test Module"); - - await Assert.That(moduleState.ModuleId).IsEqualTo(1); - await Assert.That(moduleState.PageId).IsEqualTo(1); - await Assert.That(moduleState.ModuleDefinition).IsNotNull(); - await Assert.That(moduleState.PermissionList).IsNotNull(); - await Assert.That(moduleState.PermissionList.Any(p => p.PermissionName == "Edit")).IsTrue(); - } - - [Test] - public async Task FormValidation_ValidData_Passes() - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = "Valid Name" - }; - - var validationResults = new List(); - var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); - var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(dto, context, validationResults, true); - - await Assert.That(isValid).IsTrue(); - await Assert.That(validationResults.Count).IsEqualTo(0); - } - - [Test] - public async Task FormValidation_EmptyName_Fails() - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = string.Empty - }; - - var validationResults = new List(); - var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); - var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(dto, context, validationResults, true); - - await Assert.That(isValid).IsFalse(); - await Assert.That(validationResults.Count).IsGreaterThan(0); - } - - [Test] - public async Task FormValidation_NameTooLong_Fails() - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = new string('A', 101) - }; - - var validationResults = new List(); - var context = new System.ComponentModel.DataAnnotations.ValidationContext(dto); - var isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateObject(dto, context, validationResults, true); - - await Assert.That(isValid).IsFalse(); - await Assert.That(validationResults.Any(v => v.ErrorMessage?.Contains("100") == true)).IsTrue(); - } -} diff --git a/Client.Tests/Modules/SampleModule/IndexTests.cs b/Client.Tests/Modules/SampleModule/IndexTests.cs deleted file mode 100644 index af7bf56..0000000 --- a/Client.Tests/Modules/SampleModule/IndexTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Client.Tests.Modules.SampleModule; - -public class IndexTests : BaseTest -{ - [Test] - public async Task MockService_HasTestData() - { - var mockService = TestContext.Services.GetRequiredService() as MockSampleModuleService; - - await Assert.That(mockService).IsNotNull(); - await Assert.That(mockService!.GetModuleCount()).IsEqualTo(2); - - var result = await mockService.ListAsync(1, 1, 10); - await Assert.That(result.TotalCount).IsEqualTo(2); - await Assert.That(result.Items.Count()).IsEqualTo(2); - } - - [Test] - public async Task IndexComponent_ServiceDependencies_CanBeResolved() - { - var sampleModuleService = TestContext.Services.GetService(); - var navigationManager = TestContext.Services.GetService(); - var logService = TestContext.Services.GetService(); - - await Assert.That(sampleModuleService).IsNotNull(); - await Assert.That(navigationManager).IsNotNull(); - await Assert.That(logService).IsNotNull(); - } - - [Test] - public async Task ModuleState_ShouldBeInitialized() - { - var moduleState = CreateModuleState(); - - await Assert.That(moduleState.ModuleId).IsEqualTo(1); - await Assert.That(moduleState.PageId).IsEqualTo(1); - await Assert.That(moduleState.ModuleDefinition).IsNotNull(); - await Assert.That(moduleState.ModuleDefinition?.ModuleDefinitionName).IsEqualTo("ICTAce.FileHub.SampleModule"); - await Assert.That(moduleState.PermissionList).IsNotNull(); - await Assert.That(moduleState.Settings).IsNotNull(); - } - - [Test] - public async Task PageState_ShouldBeConfigured() - { - var pageState = CreatePageState("Index"); - - await Assert.That(pageState.Action).IsEqualTo("Index"); - await Assert.That(pageState.ModuleId).IsEqualTo(1); - await Assert.That(pageState.PageId).IsEqualTo(1); - await Assert.That(pageState.Page).IsNotNull(); - await Assert.That(pageState.Alias).IsNotNull(); - await Assert.That(pageState.Site).IsNotNull(); - } - - [Test] - public async Task ServiceLayer_ListAsync_ReturnsModules() - { - var mockService = TestContext.Services.GetRequiredService() as MockSampleModuleService; - - var result = await mockService!.ListAsync(1, 1, 10); - - await Assert.That(result).IsNotNull(); - await Assert.That(result.Items).IsNotNull(); - await Assert.That(result.Items.Any(m => m.Name == "Test Module 1")).IsTrue(); - await Assert.That(result.Items.Any(m => m.Name == "Test Module 2")).IsTrue(); - } - - [Test] - public async Task ServiceLayer_DeleteAsync_RemovesModule() - { - var mockService = TestContext.Services.GetRequiredService() as MockSampleModuleService; - - var initialCount = mockService!.GetModuleCount(); - await mockService.DeleteAsync(1, 1); - var finalCount = mockService.GetModuleCount(); - - await Assert.That(finalCount).IsEqualTo(initialCount - 1); - } - - [Test] - public async Task ServiceLayer_ListAsync_SupportsPagination() - { - var mockService = TestContext.Services.GetRequiredService() as MockSampleModuleService; - - mockService!.AddTestData(new GetSampleModuleDto - { - Id = 3, - ModuleId = 1, - Name = "Test Module 3", - CreatedBy = "Test User", - CreatedOn = DateTime.Now, - ModifiedBy = "Test User", - ModifiedOn = DateTime.Now - }); - - var page1 = await mockService.ListAsync(1, 1, 2); - var page2 = await mockService.ListAsync(1, 2, 2); - - await Assert.That(page1.Items.Count()).IsEqualTo(2); - await Assert.That(page2.Items.Count()).IsEqualTo(1); - await Assert.That(page1.TotalCount).IsEqualTo(3); - } -} diff --git a/Client/GlobalUsings.cs b/Client/GlobalUsings.cs index fba3ce1..f691c5e 100644 --- a/Client/GlobalUsings.cs +++ b/Client/GlobalUsings.cs @@ -4,6 +4,7 @@ global using ICTAce.FileHub.Services; global using ICTAce.FileHub.Services.Common; global using Microsoft.AspNetCore.Components; +global using Microsoft.AspNetCore.Components.Forms; global using Microsoft.AspNetCore.Components.Web; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Localization; diff --git a/Client/Modules/FileHub/Edit.razor b/Client/Modules/FileHub/Edit.razor index df9b6f2..9b36f0f 100644 --- a/Client/Modules/FileHub/Edit.razor +++ b/Client/Modules/FileHub/Edit.razor @@ -1,20 +1,195 @@ @namespace ICTAce.FileHub @inherits ModuleBase -
+ + +
-
- +
+
+

@(PageState.Action == "Add" ? "Add New File" : "Edit File")

+
+
+ +
+ +
+
+ + +
+ @if (_selectedFile != null) + { +
+ File selected: @_selectedFile.Name (@FormatFileSize(_selectedFile.Size)) +
+ } + @if (PageState.Action == "Edit" && !string.IsNullOrEmpty(_fileName)) + { +
+ Current file: @GetDisplayFileName(_fileName) +
+ } +
File is required
+
Maximum file size: 100MB
+
+
+ +
+ +
+ +
Name is required (max 100 characters)
+
+
+ +
+
- +
+ + +
+ @if (_selectedImage != null) + { +
+ Image selected: @_selectedImage.Name - size: @FormatFileSize(_selectedImage.Size) +
+ } + @if (PageState.Action == "Edit" && !string.IsNullOrEmpty(_imageName)) + { +
+ Current image: @GetDisplayFileName(_imageName) +
+ } + +
Optional - Upload thumbnail/preview image (JPG, PNG, GIF - Max 10MB)
+
+
+
+ +
+ +
Optional - max 1000 characters
+
+
+ @if (string.Equals(PageState.Action, "Edit", StringComparison.Ordinal)) + { +
+ +
+ +
+
+ } +
+ +
+ @if (_isLoadingCategories) + { +

Loading categories...

+ } + else + { +
+ + + + + +
+
+ @if (_selectedCategories != null && _selectedCategories.Any()) + { + Selected: @string.Join(", ", _selectedCategories.OfType().Where(c => c.Id > 0).Select(c => c.Name)) + } + else + { + No categories selected + } +
+ }
- - @Localizer["Cancel"] + +
+ + + @Localizer["Cancel"] + + @if (PageState.Action == "Edit") + { + + } +
+

@if (PageState.Action == "Edit") { - + } diff --git a/Client/Modules/FileHub/Edit.razor.cs b/Client/Modules/FileHub/Edit.razor.cs index 5dfd8bc..551c565 100644 --- a/Client/Modules/FileHub/Edit.razor.cs +++ b/Client/Modules/FileHub/Edit.razor.cs @@ -1,10 +1,14 @@ // Licensed to ICTAce under the MIT license. +using Microsoft.AspNetCore.Components.Forms; +using Radzen; + namespace ICTAce.FileHub; public partial class Edit { - [Inject] protected ISampleModuleService FileHubService { get; set; } = default!; + [Inject] protected Services.IFileService FileService { get; set; } = default!; + [Inject] protected ICategoryService CategoryService { get; set; } = default!; [Inject] protected NavigationManager NavigationManager { get; set; } = default!; [Inject] protected IStringLocalizer Localizer { get; set; } = default!; @@ -12,75 +16,339 @@ public partial class Edit public override string Actions => "Add,Edit"; - public override string Title => "Manage FileHub"; + public override string Title => "Manage File"; public override List Resources => [ - new Stylesheet(ModulePath() + "Module.css") + new Stylesheet(ModulePath() + "Module.css"), + new Script("_content/Radzen.Blazor/Radzen.Blazor.js") ]; private ElementReference form; private bool _validated; + private bool _isSaving; + + private IBrowserFile? _selectedFile; + private IBrowserFile? _selectedImage; private int _id; private string _name = string.Empty; + private string _fileName = string.Empty; + private string _imageName = string.Empty; + private string? _description; + private string _fileSize = string.Empty; + private int _downloads; + private string _createdby = string.Empty; private DateTime _createdon; private string _modifiedby = string.Empty; private DateTime _modifiedon; + private List _treeData = []; + private ListCategoryDto _rootNode = new() { Name = "Categories" }; + private IEnumerable? _selectedCategories; + private bool _isLoadingCategories; + protected override async Task OnInitializedAsync() { try { + await LoadCategories(); + if (string.Equals(PageState.Action, "Edit", StringComparison.Ordinal)) { _id = Int32.Parse(PageState.QueryString["id"], System.Globalization.CultureInfo.InvariantCulture); - var filehub = await FileHubService.GetAsync(_id, ModuleState.ModuleId).ConfigureAwait(true); - if (filehub != null) + var file = await FileService.GetAsync(_id, ModuleState.ModuleId).ConfigureAwait(true); + if (file != null) { - _name = filehub.Name; - _createdby = filehub.CreatedBy; - _createdon = filehub.CreatedOn; - _modifiedby = filehub.ModifiedBy; - _modifiedon = filehub.ModifiedOn; + _name = file.Name; + _fileName = file.FileName; + _imageName = file.ImageName; + _description = file.Description; + _fileSize = file.FileSize; + _downloads = file.Downloads; + _createdby = file.CreatedBy; + _createdon = file.CreatedOn; + _modifiedby = file.ModifiedBy; + _modifiedon = file.ModifiedOn; + + if (file.CategoryIds.Any()) + { + var selectedCats = GetAllCategories().Where(c => file.CategoryIds.Contains(c.Id)).ToList(); + _selectedCategories = selectedCats.Cast(); + } } } } catch (Exception ex) { - await logger.LogError(ex, "Error Loading FileHub {Id} {Error}", _id, ex.Message).ConfigureAwait(true); + await logger.LogError(ex, "Error Loading File {Id} {Error}", _id, ex.Message).ConfigureAwait(true); AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error); } } + private void OnFileSelected(InputFileChangeEventArgs e) + { + try + { + var file = e.File; + + const long maxFileSize = 100 * 1024 * 1024; + if (file.Size > maxFileSize) + { + AddModuleMessage("File size exceeds 100MB limit", MessageType.Error); + _selectedFile = null; + return; + } + + _selectedFile = file; + + if (string.IsNullOrEmpty(_name)) + { + _name = Path.GetFileNameWithoutExtension(file.Name); + } + + StateHasChanged(); + } + catch (Exception ex) + { + AddModuleMessage("Error selecting file", MessageType.Error); + _selectedFile = null; + } + } + + private void OnImageSelected(InputFileChangeEventArgs e) + { + try + { + var file = e.File; + + const long maxImageSize = 10 * 1024 * 1024; + if (file.Size > maxImageSize) + { + AddModuleMessage("Image size exceeds 10MB limit", MessageType.Error); + _selectedImage = null; + return; + } + + if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + AddModuleMessage("Please select a valid image file", MessageType.Error); + _selectedImage = null; + return; + } + + _selectedImage = file; + StateHasChanged(); + } + catch (Exception ex) + { + AddModuleMessage("Error selecting image", MessageType.Error); + _selectedImage = null; + } + } + + private static string FormatFileSize(long bytes) + { + string[] sizes = ["B", "KB", "MB", "GB", "TB"]; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + + private static string GetDisplayFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + return string.Empty; + } + + var parts = fileName.Split('_', 2); + + if (parts.Length >= 2 && parts[0].Length == 36 && Guid.TryParse(parts[0], out _)) + { + return parts[1]; + } + + return fileName; + } + + private async Task LoadCategories() + { + try + { + _isLoadingCategories = true; + var categories = await CategoryService.ListAsync(ModuleState.ModuleId, pageNumber: 1, pageSize: int.MaxValue); + CreateTreeStructure(categories); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Categories {Error}", ex.Message).ConfigureAwait(true); + AddModuleMessage("Failed to load categories", MessageType.Warning); + } + finally + { + _isLoadingCategories = false; + } + } + + private void CreateTreeStructure(PagedResult categories) + { + if (categories.Items is null || !categories.Items.Any()) + { + _treeData = []; + _rootNode = new ListCategoryDto + { + Id = 0, + Name = "Categories", + ParentId = -1, + ViewOrder = 0, + IsExpanded = true, + Children = [] + }; + return; + } + + foreach (var category in categories.Items) + { + category.Children.Clear(); + } + + var categoryDict = categories.Items.ToDictionary(c => c.Id, c => c); + + _treeData = categories.Items + .Where(c => c.ParentId is null || !categoryDict.ContainsKey(c.ParentId.Value)) + .OrderBy(c => c.ViewOrder) + .ThenBy(c => c.Name, StringComparer.Ordinal) + .ToList(); + + foreach (var category in categories.Items) + { + if (category.ParentId is not null && categoryDict.TryGetValue(category.ParentId.Value, out var parent)) + { + parent.Children.Add(category); + } + } + + SortChildren(_treeData); + + _rootNode = new ListCategoryDto + { + Id = 0, + Name = "Categories", + ParentId = null, + ViewOrder = 0, + IsExpanded = true, + Children = _treeData + }; + } + + private static void SortChildren(List categories) + { + foreach (var category in categories) + { + if (category.Children.Any()) + { + category.Children = category.Children + .OrderBy(c => c.ViewOrder) + .ThenBy(c => c.Name, StringComparer.Ordinal) + .ToList(); + + SortChildren(category.Children.ToList()); + } + } + } + private async Task Save() { try { _validated = true; var interop = new Oqtane.UI.Interop(JSRuntime); + + if (PageState.Action == "Add" && _selectedFile == null) + { + AddModuleMessage("Please select a file to upload", MessageType.Warning); + return; + } + if (await interop.FormValid(form)) { - if (string.Equals(PageState.Action, "Add", StringComparison.Ordinal)) + _isSaving = true; + StateHasChanged(); + + string uploadedFileName = _fileName; + string uploadedImageName = _imageName; + string fileSize = _fileSize; + + try { - var dto = new CreateAndUpdateSampleModuleDto + if (_selectedFile != null) { - Name = _name - }; - var id = await FileHubService.CreateAsync(ModuleState.ModuleId, dto).ConfigureAwait(true); - await logger.LogInformation("FileHub Created {Id}", id).ConfigureAwait(true); + const long maxFileSize = 100 * 1024 * 1024; + using var stream = _selectedFile.OpenReadStream(maxFileSize); + uploadedFileName = await FileService.UploadFileAsync(ModuleState.ModuleId, stream, _selectedFile.Name).ConfigureAwait(true); + fileSize = FormatFileSize(_selectedFile.Size); + } + + if (_selectedImage != null) + { + const long maxImageSize = 10 * 1024 * 1024; + using var stream = _selectedImage.OpenReadStream(maxImageSize); + uploadedImageName = await FileService.UploadFileAsync(ModuleState.ModuleId, stream, _selectedImage.Name).ConfigureAwait(true); + } + + var selectedCategoryIds = _selectedCategories? + .OfType() + .Where(c => c.Id > 0) + .Select(c => c.Id) + .ToList() ?? []; + + if (string.Equals(PageState.Action, "Add", StringComparison.Ordinal)) + { + var dto = new CreateAndUpdateFileDto + { + Name = _name, + FileName = uploadedFileName, + ImageName = uploadedImageName, + Description = _description, + FileSize = fileSize, + Downloads = 0, + CategoryIds = selectedCategoryIds + }; + var id = await FileService.CreateAsync(ModuleState.ModuleId, dto).ConfigureAwait(true); + await logger.LogInformation("File Created {Id}", id).ConfigureAwait(true); + AddModuleMessage("File uploaded successfully", MessageType.Success); + } + else + { + var dto = new CreateAndUpdateFileDto + { + Name = _name, + FileName = uploadedFileName, + ImageName = uploadedImageName, + Description = _description, + FileSize = fileSize, + Downloads = 0, + CategoryIds = selectedCategoryIds + }; + var id = await FileService.UpdateAsync(_id, ModuleState.ModuleId, dto).ConfigureAwait(true); + await logger.LogInformation("File Updated {Id}", id).ConfigureAwait(true); + AddModuleMessage("File updated successfully", MessageType.Success); + } + + NavigationManager.NavigateTo(NavigateUrl()); } - else + catch (Exception uploadEx) { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = _name - }; - var id = await FileHubService.UpdateAsync(_id, ModuleState.ModuleId, dto).ConfigureAwait(true); - await logger.LogInformation("FileHub Updated {Id}", id).ConfigureAwait(true); + await logger.LogError(uploadEx, "Error Uploading File {Error}", uploadEx.Message).ConfigureAwait(true); + AddModuleMessage("Error uploading file. Please try again.", MessageType.Error); } - NavigationManager.NavigateTo(NavigateUrl()); } else { @@ -89,8 +357,50 @@ private async Task Save() } catch (Exception ex) { - await logger.LogError(ex, "Error Saving FileHub {Error}", ex.Message).ConfigureAwait(true); + await logger.LogError(ex, "Error Saving File {Error}", ex.Message).ConfigureAwait(true); AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error); } + finally + { + _isSaving = false; + StateHasChanged(); + } + } + + private async Task Delete() + { + try + { + await FileService.DeleteAsync(_id, ModuleState.ModuleId).ConfigureAwait(true); + await logger.LogInformation("File Deleted {Id}", _id).ConfigureAwait(true); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting File {Id} {Error}", _id, ex.Message).ConfigureAwait(true); + AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error); + } + } + + private IEnumerable GetAllCategories() + { + var result = new List(); + AddCategoriesRecursive(_treeData, result); + return result; + } + + private static void AddCategoriesRecursive(IEnumerable categories, List result) + { + foreach (var category in categories) + { + if (category.Id > 0) + { + result.Add(category); + } + if (category.Children.Any()) + { + AddCategoriesRecursive(category.Children, result); + } + } } } diff --git a/Client/Modules/FileHub/Index.razor b/Client/Modules/FileHub/Index.razor index 012b14f..7d5eb92 100644 --- a/Client/Modules/FileHub/Index.razor +++ b/Client/Modules/FileHub/Index.razor @@ -1,32 +1,77 @@ @namespace ICTAce.FileHub @inherits ModuleBase -@if (_filehubs == null) + + +@if (_isLoading) { -

Loading...

+
+ + + +
} else { - -
-
- @if (_filehubs.Count != 0) +
+ +
+ + @if (_files == null || !_files.Any()) { - -
-   -   - @Localizer["Name"] -
- - - - @context.Name - -
+
+ @Localizer["Message.DisplayNone"] +
} else { -

@Localizer["Message.DisplayNone"]

+ + + } } diff --git a/Client/Modules/FileHub/Index.razor.cs b/Client/Modules/FileHub/Index.razor.cs index d9a3ba3..50209c6 100644 --- a/Client/Modules/FileHub/Index.razor.cs +++ b/Client/Modules/FileHub/Index.razor.cs @@ -4,7 +4,7 @@ namespace ICTAce.FileHub; public partial class Index { - [Inject] protected ISampleModuleService FileHubService { get; set; } = default!; + [Inject] protected Services.IFileService FileService { get; set; } = default!; [Inject] protected NavigationManager NavigationManager { get; set; } = default!; [Inject] protected IStringLocalizer Localizer { get; set; } = default!; @@ -12,39 +12,67 @@ public partial class Index [ new Stylesheet(ModulePath() + "Module.css"), new Script(ModulePath() + "Module.js"), + new Script("_content/Radzen.Blazor/Radzen.Blazor.js") ]; - private List? _filehubs; + private List? _files; + private bool _isLoading = true; protected override async Task OnInitializedAsync() { try { - var pagedResult = await FileHubService.ListAsync(ModuleState.ModuleId).ConfigureAwait(true); - _filehubs = pagedResult?.Items?.ToList(); + _isLoading = true; + var pagedResult = await FileService.ListAsync(ModuleState.ModuleId, pageNumber: 1, pageSize: int.MaxValue).ConfigureAwait(true); + _files = pagedResult?.Items?.ToList(); } catch (Exception ex) { - await logger.LogError(ex, "Error Loading FileHub {Error}", ex.Message).ConfigureAwait(true); + await logger.LogError(ex, "Error Loading Files {Error}", ex.Message).ConfigureAwait(true); AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error); } + finally + { + _isLoading = false; + } } - private async Task Delete(ListSampleModuleDto filehub) + private void OnDownloadClick(ListFileDto file) { - try - { - await FileHubService.DeleteAsync(filehub.Id, ModuleState.ModuleId).ConfigureAwait(true); - await logger.LogInformation("FileHub Deleted {Id}", filehub.Id).ConfigureAwait(true); + // Increment the counter in the UI immediately for instant feedback + file.Downloads++; + StateHasChanged(); + } - var pagedResult = await FileHubService.ListAsync(ModuleState.ModuleId).ConfigureAwait(true); - _filehubs = pagedResult?.Items?.ToList(); - StateHasChanged(); + private void NavigateToEdit(int fileId) + { + NavigationManager.NavigateTo(EditUrl("id", fileId.ToString())); + } + + private void NavigateToAdd() + { + NavigationManager.NavigateTo(EditUrl("Add")); + } + + private string GetImageUrl(string imageName) + { + if (string.IsNullOrEmpty(imageName)) + { + return string.Empty; } - catch (Exception ex) + + // Use the serve endpoint for image display (no download counter increment) + return $"/api/ictace/fileHub/files/serve/{Uri.EscapeDataString(imageName)}"; + } + + private string GetDownloadUrl(string fileName) + { + if (string.IsNullOrEmpty(fileName)) { - await logger.LogError(ex, "Error Deleting FileHub {Id} {Error}", filehub.Id, ex.Message).ConfigureAwait(true); - AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error); + return string.Empty; } + + // Use the serve endpoint with download=true to increment counter + return $"/api/ictace/fileHub/files/serve/{Uri.EscapeDataString(fileName)}?download=true"; } } diff --git a/Client/Modules/SampleModule/Edit.razor b/Client/Modules/SampleModule/Edit.razor deleted file mode 100644 index 1d6d0fc..0000000 --- a/Client/Modules/SampleModule/Edit.razor +++ /dev/null @@ -1,23 +0,0 @@ -@namespace ICTAce.FileHub.SampleModule -@inherits ModuleBase - -
-
-
- -
- -
-
-
- - @if (!string.IsNullOrEmpty(_cancelUrl)) - { - @Localizer["Cancel"] - } -

- @if (_showAuditInfo) - { - - } -
diff --git a/Client/Modules/SampleModule/Edit.razor.cs b/Client/Modules/SampleModule/Edit.razor.cs deleted file mode 100644 index e1c9a3d..0000000 --- a/Client/Modules/SampleModule/Edit.razor.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.SampleModule; - -public partial class Edit -{ - [Inject] protected ISampleModuleService SampleModuleService { get; set; } = default!; - [Inject] protected NavigationManager NavigationManager { get; set; } = default!; - [Inject] protected IStringLocalizer Localizer { get; set; } = default!; - - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; - - public override string Actions => "Add,Edit"; - - public override string Title => "Manage SampleModule"; - - public override List Resources => - [ - new Stylesheet(ModulePath() + "Module.css") - ]; - - private ElementReference form; - private bool _validated; - - private int _id; - private string _name = string.Empty; - private string _createdby = string.Empty; - private DateTime _createdon; - private string _modifiedby = string.Empty; - private DateTime _modifiedon; - private string _cancelUrl = string.Empty; - private bool _showAuditInfo; - - protected override async Task OnInitializedAsync() - { - try - { - try - { - _cancelUrl = NavigateUrl(); - } - catch - { - _cancelUrl = "#"; - } - - _showAuditInfo = string.Equals(PageState.Action, "Edit", StringComparison.Ordinal); - - if (_showAuditInfo) - { - _id = Int32.Parse(PageState.QueryString["id"], System.Globalization.CultureInfo.InvariantCulture); - var sampleModule = await SampleModuleService.GetAsync(_id, ModuleState.ModuleId).ConfigureAwait(true); - if (sampleModule != null) - { - _name = sampleModule.Name; - _createdby = sampleModule.CreatedBy; - _createdon = sampleModule.CreatedOn; - _modifiedby = sampleModule.ModifiedBy; - _modifiedon = sampleModule.ModifiedOn; - } - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading SampleModule {Id} {Error}", _id, ex.Message).ConfigureAwait(true); - AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error); - } - } - - private async Task Save() - { - try - { - _validated = true; - var interop = new Oqtane.UI.Interop(JSRuntime); - if (await interop.FormValid(form)) - { - if (string.Equals(PageState.Action, "Add", StringComparison.Ordinal)) - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = _name - }; - var id = await SampleModuleService.CreateAsync(ModuleState.ModuleId, dto).ConfigureAwait(true); - - await logger.LogInformation("SampleModule Created {Id}", id).ConfigureAwait(true); - } - else - { - var dto = new CreateAndUpdateSampleModuleDto - { - Name = _name - }; - var id = await SampleModuleService.UpdateAsync(_id, ModuleState.ModuleId, dto).ConfigureAwait(true); - - await logger.LogInformation("SampleModule Updated {Id}", id).ConfigureAwait(true); - } - - try - { - NavigationManager.NavigateTo(NavigateUrl()); - } - catch - { - // Navigation may fail in test environments - } - } - else - { - AddModuleMessage(Localizer["Message.SaveValidation"], MessageType.Warning); - } - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Saving SampleModule {Error}", ex.Message).ConfigureAwait(true); - AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error); - } - } -} diff --git a/Client/Modules/SampleModule/Index.razor b/Client/Modules/SampleModule/Index.razor deleted file mode 100644 index 3645ec7..0000000 --- a/Client/Modules/SampleModule/Index.razor +++ /dev/null @@ -1,32 +0,0 @@ -@namespace ICTAce.FileHub.SampleModule -@inherits ModuleBase - -@if (_samplesModules == null) -{ -

Loading...

-} -else -{ - -
-
- @if (_samplesModules.Count != 0) - { - -
-   -   - @Localizer["Name"] -
- - - - @context.Name - -
- } - else - { -

@Localizer["Message.DisplayNone"]

- } -} diff --git a/Client/Modules/SampleModule/Index.razor.cs b/Client/Modules/SampleModule/Index.razor.cs deleted file mode 100644 index 27bcc84..0000000 --- a/Client/Modules/SampleModule/Index.razor.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.SampleModule; - -public partial class Index -{ - [Inject] protected ISampleModuleService SampleModuleService { get; set; } = default!; - [Inject] protected NavigationManager NavigationManager { get; set; } = default!; - [Inject] protected IStringLocalizer Localizer { get; set; } = default!; - - public override List Resources => - [ - new Stylesheet(ModulePath() + "Module.css"), - new Script(ModulePath() + "Module.js"), - ]; - - private List? _samplesModules; - - protected override async Task OnInitializedAsync() - { - try - { - var pagedResult = await SampleModuleService.ListAsync(ModuleState.ModuleId).ConfigureAwait(true); - _samplesModules = pagedResult?.Items?.ToList(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading SampleModule {Error}", ex.Message).ConfigureAwait(true); - AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error); - } - } - - private async Task Delete(ListSampleModuleDto sampleModule) - { - try - { - await SampleModuleService.DeleteAsync(sampleModule.Id, ModuleState.ModuleId).ConfigureAwait(true); - await logger.LogInformation("SampleModule Deleted {Id}", sampleModule.Id).ConfigureAwait(true); - var pagedResult = await SampleModuleService.ListAsync(ModuleState.ModuleId).ConfigureAwait(true); - _samplesModules = pagedResult?.Items?.ToList(); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Deleting SampleModule {Id} {Error}", sampleModule.Id, ex.Message).ConfigureAwait(true); - AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error); - } - } -} diff --git a/Client/Modules/SampleModule/ModuleInfo.cs b/Client/Modules/SampleModule/ModuleInfo.cs deleted file mode 100644 index cd2ee4d..0000000 --- a/Client/Modules/SampleModule/ModuleInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Modules.SampleModule; - -public class ModuleInfo : IModule -{ - public ModuleDefinition ModuleDefinition => new() - { - Name = "SampleModule", - Description = "Sample module", - Version = "1.0.0", - ServerManagerType = "ICTAce.FileHub.Managers.SampleModule, ICTAce.FileHub.Server.Oqtane", - ReleaseVersions = "1.0.0", - PackageName = "ICTAce.FileHub", - }; -} diff --git a/Client/Modules/SampleModule/Settings.razor b/Client/Modules/SampleModule/Settings.razor deleted file mode 100644 index 92ccfd6..0000000 --- a/Client/Modules/SampleModule/Settings.razor +++ /dev/null @@ -1,29 +0,0 @@ -@namespace ICTAce.FileHub.SampleModule -@inherits ModuleBase - -
-
- -
- -
-
-
-
- -
-
-
diff --git a/Client/Modules/SampleModule/Settings.razor.cs b/Client/Modules/SampleModule/Settings.razor.cs deleted file mode 100644 index c3671dc..0000000 --- a/Client/Modules/SampleModule/Settings.razor.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.SampleModule; - -public partial class Settings : ModuleBase -{ - [Inject] - protected ISettingService SettingService { get; set; } = default!; - - [Inject] - protected IStringLocalizer Localizer { get; set; } = default!; - - private const string ResourceType = "ICTAce.FileHub.SampleModule.Settings, ICTAce.FileHub.Client.Oqtane"; - - public override string Title => "SampleModule Settings"; - - private string _value = string.Empty; - - protected override async Task OnInitializedAsync() - { - try - { - var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); - _value = SettingService.GetSetting(settings, "SettingName", string.Empty); - } - catch (Exception ex) - { - await HandleErrorAsync(ex); - } - } - - public async Task UpdateSettings() - { - try - { - var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); - SettingService.SetSetting(settings, "SettingName", _value); - await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); - - AddModuleMessage("Settings updated successfully", MessageType.Success); - } - catch (Exception ex) - { - await HandleErrorAsync(ex); - } - } - - private async Task HandleErrorAsync(Exception ex) - { - AddModuleMessage(ex.Message, MessageType.Error); - await Task.CompletedTask; - } -} diff --git a/Client/Resources/ICTAce.FileHub.SampleModule/Edit.resx b/Client/Resources/ICTAce.FileHub.SampleModule/Edit.resx deleted file mode 100644 index d6028e1..0000000 --- a/Client/Resources/ICTAce.FileHub.SampleModule/Edit.resx +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Name: - - - Enter the name - - - Save - - - Cancel - - - Please Provide All Required Information - - - Error Saving SampleModule - - - Error Loading SampleModule - - \ No newline at end of file diff --git a/Client/Resources/ICTAce.FileHub.SampleModule/Index.resx b/Client/Resources/ICTAce.FileHub.SampleModule/Index.resx deleted file mode 100644 index 735ef7d..0000000 --- a/Client/Resources/ICTAce.FileHub.SampleModule/Index.resx +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Name - - - Add SampleModule - - - Edit - - - Delete - - - Delete SampleModule - - - Are You Sure You Wish To Delete This SampleModule? - - - No SampleModule To Display - - - Error Loading SampleModule - - - Error Deleting SampleModule - - \ No newline at end of file diff --git a/Client/Resources/ICTAce.FileHub.SampleModule/Settings.resx b/Client/Resources/ICTAce.FileHub.SampleModule/Settings.resx deleted file mode 100644 index 83dc88f..0000000 --- a/Client/Resources/ICTAce.FileHub.SampleModule/Settings.resx +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Name: - - - Enter a value - - \ No newline at end of file diff --git a/Client/Resources/ICTAce.FileHub/Edit.resx b/Client/Resources/ICTAce.FileHub/Edit.resx index d6028e1..e786d5d 100644 --- a/Client/Resources/ICTAce.FileHub/Edit.resx +++ b/Client/Resources/ICTAce.FileHub/Edit.resx @@ -117,25 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Name: - - - Enter the name - Save Cancel - - Please Provide All Required Information - - - Error Saving SampleModule - - - Error Loading SampleModule - \ No newline at end of file diff --git a/Client/Resources/ICTAce.FileHub/Index.resx b/Client/Resources/ICTAce.FileHub/Index.resx index 735ef7d..cd3e0bb 100644 --- a/Client/Resources/ICTAce.FileHub/Index.resx +++ b/Client/Resources/ICTAce.FileHub/Index.resx @@ -117,31 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Name - - - Add SampleModule - - - Edit - - - Delete - - - Delete SampleModule - - - Are You Sure You Wish To Delete This SampleModule? - - No SampleModule To Display - - - Error Loading SampleModule - - - Error Deleting SampleModule + No File To Display \ No newline at end of file diff --git a/Client/Services/FileService.cs b/Client/Services/FileService.cs new file mode 100644 index 0000000..9598fbe --- /dev/null +++ b/Client/Services/FileService.cs @@ -0,0 +1,91 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Services; + +public record GetFileDto +{ + public int Id { get; set; } + public int ModuleId { get; set; } + public required string Name { get; set; } + public required string FileName { get; set; } + public required string ImageName { get; set; } + public string? Description { get; set; } + public required string FileSize { get; set; } + public int Downloads { get; set; } + public List CategoryIds { get; set; } = []; + + public required string CreatedBy { get; set; } + public required DateTime CreatedOn { get; set; } + public required string ModifiedBy { get; set; } + public required DateTime ModifiedOn { get; set; } +} + +public record ListFileDto +{ + public int Id { get; set; } + public required string Name { get; set; } + public required string FileName { get; set; } + public required string ImageName { get; set; } + public string? Description { get; set; } + public required string FileSize { get; set; } + public int Downloads { get; set; } + public required DateTime CreatedOn { get; set; } +} + +public record CreateAndUpdateFileDto +{ + [Required(ErrorMessage = "Name is required")] + [StringLength(100, MinimumLength = 1, ErrorMessage = "Name must be between 1 and 100 characters")] + public string Name { get; set; } = string.Empty; + + [Required(ErrorMessage = "FileName is required")] + [StringLength(255, MinimumLength = 1, ErrorMessage = "FileName must be between 1 and 255 characters")] + public string FileName { get; set; } = string.Empty; + + [Required(ErrorMessage = "ImageName is required")] + [StringLength(255, MinimumLength = 1, ErrorMessage = "ImageName must be between 1 and 255 characters")] + public string ImageName { get; set; } = string.Empty; + + [StringLength(1000, ErrorMessage = "Description must not exceed 1000 characters")] + public string? Description { get; set; } + + [Required(ErrorMessage = "FileSize is required")] + [StringLength(12, MinimumLength = 1, ErrorMessage = "FileSize must be between 1 and 12 characters")] + public string FileSize { get; set; } = string.Empty; + + public int Downloads { get; set; } + + public List CategoryIds { get; set; } = []; +} + +public interface IFileService +{ + Task GetAsync(int id, int moduleId); + Task> ListAsync(int moduleId, int pageNumber = 1, int pageSize = 10); + Task CreateAsync(int moduleId, CreateAndUpdateFileDto dto); + Task UpdateAsync(int id, int moduleId, CreateAndUpdateFileDto dto); + Task DeleteAsync(int id, int moduleId); + Task UploadFileAsync(int moduleId, Stream fileStream, string fileName); +} + +public class FileService(HttpClient http, SiteState siteState) + : ModuleService(http, siteState, "ictace/fileHub/files"), + IFileService +{ + private readonly HttpClient _http = http; + + public async Task UploadFileAsync(int moduleId, Stream fileStream, string fileName) + { + var url = CreateAuthorizationPolicyUrl($"{CreateApiUrl("ictace/fileHub/files")}/upload?moduleId={moduleId}", EntityNames.Module, moduleId); + + using var content = new MultipartFormDataContent(); + using var streamContent = new StreamContent(fileStream); + streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + content.Add(streamContent, "file", fileName); + + var response = await _http.PostAsync(url, content).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } +} diff --git a/Client/Services/SampleModuleService.cs b/Client/Services/SampleModuleService.cs deleted file mode 100644 index f41aba1..0000000 --- a/Client/Services/SampleModuleService.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Services; - -public record GetSampleModuleDto -{ - public int Id { get; set; } - public int ModuleId { get; set; } - public required string Name { get; set; } - - public required string CreatedBy { get; set; } - public required DateTime CreatedOn { get; set; } - public required string ModifiedBy { get; set; } - public required DateTime ModifiedOn { get; set; } -} - -public record ListSampleModuleDto -{ - public int Id { get; set; } - public required string Name { get; set; } -} - -public record CreateAndUpdateSampleModuleDto -{ - [Required(ErrorMessage = "Name is required")] - [StringLength(100, MinimumLength = 1, ErrorMessage = "Name must be between 1 and 100 characters")] - public string Name { get; set; } = string.Empty; -} - -public interface ISampleModuleService -{ - Task GetAsync(int id, int moduleId); - Task> ListAsync(int moduleId, int pageNumber = 1, int pageSize = 10); - Task CreateAsync(int moduleId, CreateAndUpdateSampleModuleDto dto); - Task UpdateAsync(int id, int moduleId, CreateAndUpdateSampleModuleDto dto); - Task DeleteAsync(int id, int moduleId); -} - -public class SampleModuleService(HttpClient http, SiteState siteState) - : ModuleService(http, siteState, "company/sampleModules"), - ISampleModuleService -{ -} diff --git a/Client/Startup/ClientStartup.cs b/Client/Startup/ClientStartup.cs index 5611d54..f515ed3 100644 --- a/Client/Startup/ClientStartup.cs +++ b/Client/Startup/ClientStartup.cs @@ -8,14 +8,14 @@ public class ClientStartup : IClientStartup { public void ConfigureServices(IServiceCollection services) { - if (!services.Any(s => s.ServiceType == typeof(ISampleModuleService))) + if (!services.Any(s => s.ServiceType == typeof(ICategoryService))) { - services.AddScoped(); + services.AddScoped(); } - if (!services.Any(s => s.ServiceType == typeof(ICategoryService))) + if (!services.Any(s => s.ServiceType == typeof(Services.IFileService))) { - services.AddScoped(); + services.AddScoped(); } services.AddRadzenComponents(); diff --git a/Client/_Imports.razor b/Client/_Imports.razor index 49aa3c3..e6311ec 100644 --- a/Client/_Imports.razor +++ b/Client/_Imports.razor @@ -1,5 +1,6 @@ @using ICTAce.FileHub.Services @using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.Extensions.Localization diff --git a/README.md b/README.md index d3ed012..ca9cd89 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A modular file management system built as an **Oqtane CMS module** using modern This project implements a modern, maintainable architecture using: ### Core Architecture -- **Vertical Slice Architecture (VSA)**: Features are organized by business capability rather than technical layers. Each feature slice (Create, Update, Delete, Get, List) contains its own handlers, requests, responses, and mapping logic in a cohesive unit under `Server/Features/SampleModule/`. +- **Vertical Slice Architecture (VSA)**: Features are organized by business capability rather than technical layers. Each feature slice (Create, Update, Delete, Get, List) contains its own handlers, requests, responses, and mapping logic. - **CQRS Pattern**: Clear separation between commands (Create, Update, Delete) and queries (Get, List) using MediatR handlers with dedicated base classes (`CommandHandlerBase`, `QueryHandlerBase`). - **MediatR**: Implements the mediator pattern for in-process messaging, decoupling request handling across feature slices and enabling clean separation of concerns. diff --git a/Server.Tests/Features/Common/EdgeCaseTests.cs b/Server.Tests/Features/Common/EdgeCaseTests.cs deleted file mode 100644 index 97ddd5d..0000000 --- a/Server.Tests/Features/Common/EdgeCaseTests.cs +++ /dev/null @@ -1,402 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -using CategoryHandlers = ICTAce.FileHub.Features.Categories; - -namespace ICTAce.FileHub.Server.Tests.Features.Common; - -/// -/// Tests for edge cases and error handling scenarios. -/// -public class EdgeCaseTests : HandlerTestBase -{ - #region Null and Empty String Tests - - [Test] - public async Task Create_WithEmptyName_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = string.Empty - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(string.Empty); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Update_WithEmptyName_UpdatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = string.Empty - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(string.Empty); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Create_WithVeryLongName_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var longName = new string('A', 500); - - // Act - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = longName - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(longName); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Create_WithSpecialCharacters_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var specialName = "Test!@#$%^&*()_+-={}[]|\\:\";<>?,./~`"; - - // Act - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = specialName - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(specialName); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Create_WithUnicodeCharacters_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var unicodeName = "Test 你好 мир 🚀 emoji"; - - // Act - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = unicodeName - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(unicodeName); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Invalid ID Tests - - [Test] - public async Task Get_WithZeroId_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new GetSampleModuleRequest - { - Id = 0, - ModuleId = 1 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Get_WithNegativeId_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new GetSampleModuleRequest - { - Id = -1, - ModuleId = 1 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Delete_WithZeroId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new DeleteSampleModuleRequest - { - Id = 0, - ModuleId = 1 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Update_WithZeroId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new UpdateSampleModuleRequest - { - Id = 0, - ModuleId = 1, - Name = "Test" - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Invalid ModuleId Tests - - [Test] - public async Task Create_WithZeroModuleId_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - ModuleId 0 might be valid in some systems - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 0, - Name = "Test" - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_WithZeroModuleId_ReturnsEmptyList() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Module 1")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new ListSampleModuleRequest - { - ModuleId = 0, - PageNumber = 1, - PageSize = 10 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Concurrent Operations - - [Test] - public async Task MultipleCreates_Concurrent_AllSucceed() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - Create 10 entities concurrently - var tasks = Enumerable.Range(1, 10).Select(i => - handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = $"Concurrent {i}" - }, CancellationToken.None) - ); - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - - // Assert - All creates succeeded with unique IDs - await Assert.That(results.All(r => r > 0)).IsTrue(); - await Assert.That(results.Distinct().Count()).IsEqualTo(10); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(10); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region SQL Injection Prevention - - [Test] - public async Task Create_WithSqlInjectionAttempt_TreatsAsLiteralString() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var sqlInjection = "'; DROP TABLE SampleModule; --"; - - // Act - var result = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = sqlInjection - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - Entity created with SQL injection as literal text - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo(sqlInjection); - - // Verify table still exists - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Category-Specific Edge Cases - - [Test] - public async Task Category_WithNegativeViewOrder_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CategoryHandlers.CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new CategoryHandlers.CreateCategoryRequest - { - ModuleId = 1, - Name = "Test", - ViewOrder = -1, - ParentId = null - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Category_WithNonExistentParentId_CreatesSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CategoryHandlers.CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - ParentId 999 doesn't exist - var result = await handler.Handle(new CategoryHandlers.CreateCategoryRequest - { - ModuleId = 1, - Name = "Orphan Category", - ViewOrder = 1, - ParentId = 999 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - Creates successfully (orphan category) - await Assert.That(result).IsGreaterThan(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion -} diff --git a/Server.Tests/Features/Common/HandlerBaseTests.cs b/Server.Tests/Features/Common/HandlerBaseTests.cs deleted file mode 100644 index d58b609..0000000 --- a/Server.Tests/Features/Common/HandlerBaseTests.cs +++ /dev/null @@ -1,446 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; - -namespace ICTAce.FileHub.Server.Tests.Features.Common; - -/// -/// Tests for the generic HandlerBase methods to ensure they work correctly -/// across different entity types and scenarios. -/// -public class HandlerBaseTests : HandlerTestBase -{ - #region HandleCreateAsync Tests - - [Test] - public async Task HandleCreateAsync_WithValidRequest_CreatesEntity() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Test Module" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var entity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - await Assert.That(entity!.Name).IsEqualTo("Test Module"); - await Assert.That(entity.ModuleId).IsEqualTo(1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleCreateAsync_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Test Module" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleCreateAsync_AutoAssignsId() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - Create multiple entities - var id1 = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "First" - }, CancellationToken.None).ConfigureAwait(false); - - var id2 = await handler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Second" - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - IDs are auto-incremented - await Assert.That(id1).IsGreaterThan(0); - await Assert.That(id2).IsGreaterThan(id1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region HandleGetAsync Tests - - [Test] - public async Task HandleGetAsync_WithExistingEntity_ReturnsDto() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Test")).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.Id).IsEqualTo(1); - await Assert.That(result.Name).IsEqualTo("Test"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleGetAsync_WithNonExistentId_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest - { - Id = 999, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleGetAsync_WithUnauthorizedUser_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new GetSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region HandleDeleteAsync Tests - - [Test] - public async Task HandleDeleteAsync_WithExistingEntity_DeletesAndReturnsId() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleDeleteAsync_WithNonExistentId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 999, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleDeleteAsync_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - // Verify entity still exists - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region HandleListAsync Tests - - [Test] - public async Task HandleListAsync_ReturnsPagedResults() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "First"), - CreateTestEntity(id: 2, moduleId: 1, name: "Second"), - CreateTestEntity(id: 3, moduleId: 1, name: "Third")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 2 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.Items.Count()).IsEqualTo(2); - await Assert.That(result.TotalCount).IsEqualTo(3); - await Assert.That(result.PageNumber).IsEqualTo(1); - await Assert.That(result.PageSize).IsEqualTo(2); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleListAsync_WithCustomOrdering_SortsCorrectly() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Zebra"), - CreateTestEntity(id: 2, moduleId: 1, name: "Apple"), - CreateTestEntity(id: 3, moduleId: 1, name: "Mango")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - var items = result!.Items.ToList(); - await Assert.That(result).IsNotNull(); - await Assert.That(items[0].Name).IsEqualTo("Apple"); - await Assert.That(items[1].Name).IsEqualTo("Mango"); - await Assert.That(items[2].Name).IsEqualTo("Zebra"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleListAsync_WithUnauthorizedUser_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region HandleUpdateAsync Tests - - [Test] - public async Task HandleUpdateAsync_WithValidRequest_UpdatesEntity() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = "Updated" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Updated"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleUpdateAsync_WithNonExistentId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 999, - ModuleId = 1, - Name = "Updated" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task HandleUpdateAsync_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = "Updated" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Original"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion -} diff --git a/Server.Tests/Features/Common/PaginationTests.cs b/Server.Tests/Features/Common/PaginationTests.cs deleted file mode 100644 index a8ac5cb..0000000 --- a/Server.Tests/Features/Common/PaginationTests.cs +++ /dev/null @@ -1,243 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using SampleModuleHandlers = ICTAce.FileHub.Features.SampleModule; -using CategoryHandlers = ICTAce.FileHub.Features.Categories; -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; - -namespace ICTAce.FileHub.Server.Tests.Features.Common; - -/// -/// Tests for pagination functionality across all list operations. -/// -public class PaginationTests : HandlerTestBase -{ - [Test] - public async Task List_FirstPage_ReturnsCorrectItems() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Item 1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Item 2"), - CreateTestEntity(id: 3, moduleId: 1, name: "Item 3"), - CreateTestEntity(id: 4, moduleId: 1, name: "Item 4"), - CreateTestEntity(id: 5, moduleId: 1, name: "Item 5")).ConfigureAwait(false); - - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 3 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.PageNumber).IsEqualTo(1); - await Assert.That(result.PageSize).IsEqualTo(3); - await Assert.That(result.TotalCount).IsEqualTo(5); - await Assert.That(result.Items.Count()).IsEqualTo(3); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_LastPageWithFewerItems_ReturnsRemainingItems() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Item 1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Item 2"), - CreateTestEntity(id: 3, moduleId: 1, name: "Item 3"), - CreateTestEntity(id: 4, moduleId: 1, name: "Item 4"), - CreateTestEntity(id: 5, moduleId: 1, name: "Item 5")).ConfigureAwait(false); - - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - Request page 2 with page size 3 (should return 2 items) - var result = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 2, - PageSize = 3 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.PageNumber).IsEqualTo(2); - await Assert.That(result.PageSize).IsEqualTo(3); - await Assert.That(result.TotalCount).IsEqualTo(5); - await Assert.That(result.Items.Count()).IsEqualTo(2); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_EmptyResultSet_ReturnsEmptyList() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(0); - await Assert.That(result.Items.Count()).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_PageSizeGreaterThanTotalCount_ReturnsAllItems() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Item 1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Item 2")).ConfigureAwait(false); - - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 100 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(2); - await Assert.That(result.Items.Count()).IsEqualTo(2); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_OrderingConsistentAcrossPages() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Zebra"), - CreateTestEntity(id: 2, moduleId: 1, name: "Apple"), - CreateTestEntity(id: 3, moduleId: 1, name: "Mango"), - CreateTestEntity(id: 4, moduleId: 1, name: "Banana"), - CreateTestEntity(id: 5, moduleId: 1, name: "Orange")).ConfigureAwait(false); - - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - Get Page 1 - var page1 = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 2 - }, CancellationToken.None).ConfigureAwait(false); - - // Act - Get Page 2 - var page2 = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 2, - PageSize = 2 - }, CancellationToken.None).ConfigureAwait(false); - - // Act - Get Page 3 - var page3 = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 3, - PageSize = 2 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - Alphabetical ordering maintained across pages - var page1List = page1!.Items.ToList(); - var page2List = page2!.Items.ToList(); - var page3List = page3!.Items.ToList(); - - await Assert.That(page1List[0].Name).IsEqualTo("Apple"); - await Assert.That(page1List[1].Name).IsEqualTo("Banana"); - await Assert.That(page2List[0].Name).IsEqualTo("Mango"); - await Assert.That(page2List[1].Name).IsEqualTo("Orange"); - await Assert.That(page3List[0].Name).IsEqualTo("Zebra"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_TotalCountAccurate() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - Enumerable.Range(1, 50).Select(i => - CreateTestEntity(id: i, moduleId: 1, name: $"Item {i}")).ToArray()).ConfigureAwait(false); - - var handler = new SampleModuleHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new SampleModuleHandlers.ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(50); - await Assert.That(result.Items.Count()).IsEqualTo(10); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task CategoryList_OrderedByViewOrderThenName() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.CategoryTestHelpers.SeedQueryDataAsync(options, - Helpers.CategoryTestHelpers.CreateTestEntity(id: 1, name: "Zebra", viewOrder: 2), - Helpers.CategoryTestHelpers.CreateTestEntity(id: 2, name: "Apple", viewOrder: 1), - Helpers.CategoryTestHelpers.CreateTestEntity(id: 3, name: "Banana", viewOrder: 1)).ConfigureAwait(false); - - var handler = new CategoryHandlers.ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - var result = await handler.Handle(new CategoryHandlers.ListCategoryRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - ViewOrder 1 items first (Apple, Banana), then ViewOrder 2 (Zebra) - var items = result!.Items.ToList(); - await Assert.That(result).IsNotNull(); - await Assert.That(items[0].Name).IsEqualTo("Apple"); - await Assert.That(items[1].Name).IsEqualTo("Banana"); - await Assert.That(items[2].Name).IsEqualTo("Zebra"); - - await connection.CloseAsync().ConfigureAwait(false); - } -} diff --git a/Server.Tests/Features/Integration/CrudWorkflowTests.cs b/Server.Tests/Features/Integration/CrudWorkflowTests.cs deleted file mode 100644 index 1ab3dc3..0000000 --- a/Server.Tests/Features/Integration/CrudWorkflowTests.cs +++ /dev/null @@ -1,352 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -using CategoryHandlers = ICTAce.FileHub.Features.Categories; - -namespace ICTAce.FileHub.Server.Tests.Features.Integration; - -/// -/// Integration tests that verify complete CRUD workflows work correctly end-to-end. -/// -public class CrudWorkflowTests : HandlerTestBase -{ - [Test] - [SuppressMessage("Maintainability", "MA0051:MethodTooLong", Justification = "Integration test covering full CRUD workflow")] - public async Task CompleteWorkflow_CreateGetUpdateDelete_AllOperationsSucceed() - { - // Arrange - var (connection, commandOptions) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var (_, queryOptions) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var createHandler = new CreateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var getHandler = new GetHandler( - CreateQueryHandlerServices(queryOptions, isAuthorized: true)); - var updateHandler = new UpdateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var deleteHandler = new DeleteHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - - // Act & Assert - CREATE - var createRequest = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "New Entity", - }; - var createdId = await createHandler.Handle(createRequest, CancellationToken.None).ConfigureAwait(false); - await Assert.That(createdId).IsGreaterThan(0); - - // Seed query database for GET - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(queryOptions, - CreateTestEntity(id: createdId, moduleId: 1, name: "New Entity")).ConfigureAwait(false); - - // Act & Assert - GET - var getRequest = new GetSampleModuleRequest - { - Id = createdId, - ModuleId = 1, - }; - var getResult = await getHandler.Handle(getRequest, CancellationToken.None).ConfigureAwait(false); - await Assert.That(getResult).IsNotNull(); - await Assert.That(getResult!.Name).IsEqualTo("New Entity"); - - // Act & Assert - UPDATE - var updateRequest = new UpdateSampleModuleRequest - { - Id = createdId, - ModuleId = 1, - Name = "Updated Entity", - }; - var updateResult = await updateHandler.Handle(updateRequest, CancellationToken.None).ConfigureAwait(false); - await Assert.That(updateResult).IsEqualTo(createdId); - - var updatedEntity = await GetFromCommandDbAsync(commandOptions, createdId).ConfigureAwait(false); - await Assert.That(updatedEntity!.Name).IsEqualTo("Updated Entity"); - - // Act & Assert - DELETE - var deleteRequest = new DeleteSampleModuleRequest - { - Id = createdId, - ModuleId = 1, - }; - var deleteResult = await deleteHandler.Handle(deleteRequest, CancellationToken.None).ConfigureAwait(false); - await Assert.That(deleteResult).IsEqualTo(createdId); - - var deletedEntity = await GetFromCommandDbAsync(commandOptions, createdId).ConfigureAwait(false); - await Assert.That(deletedEntity).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - [SuppressMessage("Maintainability", "MA0051:MethodTooLong", Justification = "Integration test covering full CRUD workflow")] - public async Task CreateMultiple_List_ReturnsPaginatedResults() - { - // Arrange - var (connection, queryOptions) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(queryOptions, - CreateTestEntity(id: 1, moduleId: 1, name: "Entity 1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Entity 2"), - CreateTestEntity(id: 3, moduleId: 1, name: "Entity 3"), - CreateTestEntity(id: 4, moduleId: 1, name: "Entity 4"), - CreateTestEntity(id: 5, moduleId: 1, name: "Entity 5")).ConfigureAwait(false); - - var listHandler = new ListHandler( - CreateQueryHandlerServices(queryOptions, isAuthorized: true)); - - // Act - Page 1 - var page1Request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 2, - }; - var page1Result = await listHandler.Handle(page1Request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Page 1 - await Assert.That(page1Result).IsNotNull(); - await Assert.That(page1Result!.Items.Count()).IsEqualTo(2); - await Assert.That(page1Result.TotalCount).IsEqualTo(5); - await Assert.That(page1Result.PageNumber).IsEqualTo(1); - - // Act - Page 2 - var page2Request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 2, - PageSize = 2, - }; - var page2Result = await listHandler.Handle(page2Request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Page 2 - await Assert.That(page2Result).IsNotNull(); - await Assert.That(page2Result!.Items.Count()).IsEqualTo(2); - await Assert.That(page2Result.TotalCount).IsEqualTo(5); - await Assert.That(page2Result.PageNumber).IsEqualTo(2); - - // Act - Page 3 (last page with 1 item) - var page3Request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 3, - PageSize = 2, - }; - var page3Result = await listHandler.Handle(page3Request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Page 3 - await Assert.That(page3Result).IsNotNull(); - await Assert.That(page3Result!.Items.Count()).IsEqualTo(1); - await Assert.That(page3Result.TotalCount).IsEqualTo(5); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task CreateUpdateGet_VerifiesChanges() - { - // Arrange - var (connection, commandOptions) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var (_, queryOptions) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var createHandler = new CreateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var updateHandler = new UpdateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var getHandler = new GetHandler( - CreateQueryHandlerServices(queryOptions, isAuthorized: true)); - - // Act - Create - var id = await createHandler.Handle(new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Original", - }, CancellationToken.None).ConfigureAwait(false); - - // Seed query database - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(queryOptions, - CreateTestEntity(id: id, moduleId: 1, name: "Original")).ConfigureAwait(false); - - // Act - Update - await updateHandler.Handle(new UpdateSampleModuleRequest - { - Id = id, - ModuleId = 1, - Name = "Modified", - }, CancellationToken.None).ConfigureAwait(false); - - // Update query database to match command changes - using (var context = new Helpers.TestApplicationQueryContext(queryOptions)) - { - var entity = await context.SampleModule.FindAsync(id).ConfigureAwait(false); - if (entity != null) - { - entity.Name = "Modified"; - await context.SaveChangesAsync().ConfigureAwait(false); - } - } - - // Act - Get - var result = await getHandler.Handle(new GetSampleModuleRequest - { - Id = id, - ModuleId = 1, - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.Name).IsEqualTo("Modified"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task DeleteNonExistent_AfterDelete_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var deleteHandler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - First delete succeeds - var firstDeleteResult = await deleteHandler.Handle(new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1, - }, CancellationToken.None).ConfigureAwait(false); - - await Assert.That(firstDeleteResult).IsEqualTo(1); - - // Act - Second delete fails (already deleted) - var secondDeleteResult = await deleteHandler.Handle(new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1, - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(secondDeleteResult).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task MultipleCreates_AllSucceed_WithIncrementingIds() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var createHandler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - Create 5 entities - var id1 = await createHandler.Handle(new CreateSampleModuleRequest - { ModuleId = 1, Name = "Entity 1" }, CancellationToken.None).ConfigureAwait(false); - - var id2 = await createHandler.Handle(new CreateSampleModuleRequest - { ModuleId = 1, Name = "Entity 2" }, CancellationToken.None).ConfigureAwait(false); - - var id3 = await createHandler.Handle(new CreateSampleModuleRequest - { ModuleId = 1, Name = "Entity 3" }, CancellationToken.None).ConfigureAwait(false); - - var id4 = await createHandler.Handle(new CreateSampleModuleRequest - { ModuleId = 1, Name = "Entity 4" }, CancellationToken.None).ConfigureAwait(false); - - var id5 = await createHandler.Handle(new CreateSampleModuleRequest - { ModuleId = 1, Name = "Entity 5" }, CancellationToken.None).ConfigureAwait(false); - - // Assert - All IDs are unique and incrementing - await Assert.That(id1).IsGreaterThan(0); - await Assert.That(id2).IsGreaterThan(id1); - await Assert.That(id3).IsGreaterThan(id2); - await Assert.That(id4).IsGreaterThan(id3); - await Assert.That(id5).IsGreaterThan(id4); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(5); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - [SuppressMessage("Maintainability", "MA0051:MethodTooLong", Justification = "Integration test covering full CRUD workflow")] - public async Task CategoryWorkflow_CreateUpdateListDelete_CompleteSuccess() - { - // Arrange - var (connection, commandOptions) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var (_, queryOptions) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var createHandler = new CategoryHandlers.CreateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var updateHandler = new CategoryHandlers.UpdateHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - var listHandler = new CategoryHandlers.ListHandler( - CreateQueryHandlerServices(queryOptions, isAuthorized: true)); - var deleteHandler = new CategoryHandlers.DeleteHandler( - CreateCommandHandlerServices(commandOptions, isAuthorized: true)); - - // Act - Create - var id = await createHandler.Handle(new CategoryHandlers.CreateCategoryRequest - { - ModuleId = 1, - Name = "Test Category", - ViewOrder = 1, - ParentId = 0, - }, CancellationToken.None).ConfigureAwait(false); - - await Assert.That(id).IsGreaterThan(0); - - // Seed query database - await Helpers.CategoryTestHelpers.SeedQueryDataAsync(queryOptions, - Helpers.CategoryTestHelpers.CreateTestEntity(id: id, moduleId: 1, name: "Test Category", viewOrder: 1)).ConfigureAwait(false); - - // Act - Update - var updateResult = await updateHandler.Handle(new CategoryHandlers.UpdateCategoryRequest - { - Id = id, - ModuleId = 1, - Name = "Updated Category", - ViewOrder = 5, - ParentId = 0, - }, CancellationToken.None).ConfigureAwait(false); - - await Assert.That(updateResult).IsEqualTo(id); - - // Update query database - using (var context = new Helpers.TestApplicationQueryContext(queryOptions)) - { - var entity = await context.Category.FindAsync(id).ConfigureAwait(false); - if (entity != null) - { - entity.Name = "Updated Category"; - entity.ViewOrder = 5; - await context.SaveChangesAsync().ConfigureAwait(false); - } - } - - // Act - List - var listResult = await listHandler.Handle(new CategoryHandlers.ListCategoryRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10, - }, CancellationToken.None).ConfigureAwait(false); - - await Assert.That(listResult).IsNotNull(); - await Assert.That(listResult!.TotalCount).IsEqualTo(1); - var items = listResult.Items.ToList(); - await Assert.That(items[0].Name).IsEqualTo("Updated Category"); - - // Act - Delete - var deleteResult = await deleteHandler.Handle(new CategoryHandlers.DeleteCategoryRequest - { - Id = id, - ModuleId = 1, - }, CancellationToken.None).ConfigureAwait(false); - - await Assert.That(deleteResult).IsEqualTo(id); - - await connection.CloseAsync().ConfigureAwait(false); - } -} diff --git a/Server.Tests/Features/SampleModule/CreateHandlerTests.cs b/Server.Tests/Features/SampleModule/CreateHandlerTests.cs deleted file mode 100644 index 9c6ae9b..0000000 --- a/Server.Tests/Features/SampleModule/CreateHandlerTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Server.Tests.Features.SampleModule; - -public class CreateHandlerTests : HandlerTestBase -{ - [Test] - public async Task Handle_WithValidRequest_CreatesSampleModule() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Test Sample Module" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var savedEntity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(savedEntity).IsNotNull(); - await Assert.That(savedEntity!.Name).IsEqualTo("Test Sample Module"); - await Assert.That(savedEntity.ModuleId).IsEqualTo(1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Unauthorized Module" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - [Arguments("")] - [Arguments("Valid Name")] - [Arguments("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")] - public async Task Handle_WithDifferentNames_CreatesSuccessfully(string name) - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = name - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsGreaterThan(0); - - var savedEntity = await GetFromCommandDbAsync(options, result).ConfigureAwait(false); - await Assert.That(savedEntity).IsNotNull(); - await Assert.That(savedEntity!.Name).IsEqualTo(name); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithMultipleModules_CreatesAllSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var id1 = await handler.Handle(new CreateSampleModuleRequest { ModuleId = 1, Name = "Module 1" }, CancellationToken.None).ConfigureAwait(false); - var id2 = await handler.Handle(new CreateSampleModuleRequest { ModuleId = 1, Name = "Module 2" }, CancellationToken.None).ConfigureAwait(false); - var id3 = await handler.Handle(new CreateSampleModuleRequest { ModuleId = 2, Name = "Module 3" }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(id1).IsGreaterThan(0); - await Assert.That(id2).IsGreaterThan(0); - await Assert.That(id3).IsGreaterThan(0); - await Assert.That(id1).IsNotEqualTo(id2); - await Assert.That(id2).IsNotEqualTo(id3); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(3); - - await connection.CloseAsync().ConfigureAwait(false); - } -} diff --git a/Server.Tests/Features/SampleModule/DeleteHandlerTests.cs b/Server.Tests/Features/SampleModule/DeleteHandlerTests.cs deleted file mode 100644 index 9e1c300..0000000 --- a/Server.Tests/Features/SampleModule/DeleteHandlerTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -namespace ICTAce.FileHub.Server.Tests.Features.SampleModule; - -public class DeleteHandlerTests : HandlerTestBase -{ - [Test] - public async Task Handle_WithValidId_DeletesSampleModule() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var deletedEntity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(deletedEntity).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithNonExistentId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 999, // Non-existent ID - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithWrongModuleId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 2 // Wrong module ID - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_DeleteMultipleModules_DeletesAllSuccessfully() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, - CreateTestEntity(id: 1, name: "Module 1"), - CreateTestEntity(id: 2, name: "Module 2"), - CreateTestEntity(id: 3, name: "Module 3")).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - var result1 = await handler.Handle(new DeleteSampleModuleRequest { Id = 1, ModuleId = 1 }, CancellationToken.None).ConfigureAwait(false); - var result2 = await handler.Handle(new DeleteSampleModuleRequest { Id = 2, ModuleId = 1 }, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result1).IsEqualTo(1); - await Assert.That(result2).IsEqualTo(2); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(1); // Only ID 3 should remain - - await connection.CloseAsync().ConfigureAwait(false); - } -} diff --git a/Server.Tests/Features/SampleModule/GetHandlerTests.cs b/Server.Tests/Features/SampleModule/GetHandlerTests.cs deleted file mode 100644 index b975b5f..0000000 --- a/Server.Tests/Features/SampleModule/GetHandlerTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; - -namespace ICTAce.FileHub.Server.Tests.Features.SampleModule; - -public class GetHandlerTests : HandlerTestBase -{ - [Test] - public async Task Handle_WithValidId_ReturnsSampleModule() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest { ModuleId = 1, Id = 1 }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.Id).IsEqualTo(1); - await Assert.That(result.Name).IsEqualTo("Test Module"); - await Assert.That(result.ModuleId).IsEqualTo(1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithInvalidId_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest { ModuleId = 1, Id = 999 }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithUnauthorizedUser_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new GetSampleModuleRequest { ModuleId = 1, Id = 1 }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithWrongModuleId_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, CreateTestEntity()).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest { ModuleId = 2, Id = 1 }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_VerifiesAuditFields_ArePopulated() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var createdOn = DateTime.UtcNow.AddDays(-5); - var modifiedOn = DateTime.UtcNow.AddDays(-1); - - await SeedQueryDataAsync(options, - CreateTestEntity( - id: 1, - moduleId: 1, - name: "Test Module", - createdBy: "creator", - createdOn: createdOn, - modifiedBy: "modifier", - modifiedOn: modifiedOn)).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest { ModuleId = 1, Id = 1 }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.CreatedBy).IsEqualTo("creator"); - await Assert.That(result.ModifiedBy).IsEqualTo("modifier"); - await Assert.That(result.CreatedOn).IsEqualTo(createdOn); - await Assert.That(result.ModifiedOn).IsEqualTo(modifiedOn); - - await connection.CloseAsync().ConfigureAwait(false); - } -} - diff --git a/Server.Tests/Features/SampleModule/ListHandlerTests.cs b/Server.Tests/Features/SampleModule/ListHandlerTests.cs deleted file mode 100644 index e4dffff..0000000 --- a/Server.Tests/Features/SampleModule/ListHandlerTests.cs +++ /dev/null @@ -1,203 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -namespace ICTAce.FileHub.Server.Tests.Features.SampleModule; - -public class ListHandlerTests : HandlerTestBase -{ - [Test] - public async Task Handle_WithData_ReturnsPagedResult() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, - CreateTestEntity(id: 1, name: "Module 1"), - CreateTestEntity(id: 2, name: "Module 2"), - CreateTestEntity(id: 3, name: "Module 3"), - CreateTestEntity(id: 4, name: "Module 4"), - CreateTestEntity(id: 5, name: "Module 5")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(5); - await Assert.That(result.Items.Count()).IsEqualTo(5); - await Assert.That(result.PageNumber).IsEqualTo(1); - await Assert.That(result.PageSize).IsEqualTo(10); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithPagination_ReturnsCorrectPage() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, - CreateTestEntity(id: 1, name: "Alpha"), - CreateTestEntity(id: 2, name: "Bravo"), - CreateTestEntity(id: 3, name: "Charlie"), - CreateTestEntity(id: 4, name: "Delta"), - CreateTestEntity(id: 5, name: "Echo")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 2, - PageSize = 2 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(5); - await Assert.That(result.Items.Count()).IsEqualTo(2); - await Assert.That(result.PageNumber).IsEqualTo(2); - await Assert.That(result.PageSize).IsEqualTo(2); - - // Items should be "Charlie" and "Delta" (sorted alphabetically, page 2) - var items = result.Items.ToList(); - await Assert.That(items[0].Name).IsEqualTo("Charlie"); - await Assert.That(items[1].Name).IsEqualTo("Delta"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithUnauthorizedUser_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithNoData_ReturnsEmptyList() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(0); - await Assert.That(result.Items.Count()).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithMultipleModules_FiltersCorrectly() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Module 1-1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Module 1-2"), - CreateTestEntity(id: 3, moduleId: 2, name: "Module 2-1"), - CreateTestEntity(id: 4, moduleId: 2, name: "Module 2-2")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(2); - await Assert.That(result.Items.Count()).IsEqualTo(2); - - var items = result.Items.ToList(); - await Assert.That(items.All(x => x.Name.StartsWith("Module 1", StringComparison.Ordinal))).IsTrue(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_VerifiesAlphabeticalOrdering() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await SeedQueryDataAsync(options, - CreateTestEntity(id: 1, name: "Zebra"), - CreateTestEntity(id: 2, name: "Apple"), - CreateTestEntity(id: 3, name: "Mango")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNotNull(); - var items = result!.Items.ToList(); - await Assert.That(items[0].Name).IsEqualTo("Apple"); - await Assert.That(items[1].Name).IsEqualTo("Mango"); - await Assert.That(items[2].Name).IsEqualTo("Zebra"); - - await connection.CloseAsync().ConfigureAwait(false); - } -} - diff --git a/Server.Tests/Features/SampleModule/UpdateHandlerTests.cs b/Server.Tests/Features/SampleModule/UpdateHandlerTests.cs deleted file mode 100644 index e42034c..0000000 --- a/Server.Tests/Features/SampleModule/UpdateHandlerTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -namespace ICTAce.FileHub.Server.Tests.Features.SampleModule; - -public class UpdateHandlerTests : HandlerTestBase -{ - [Test] - public async Task Handle_WithValidRequest_UpdatesSampleModule() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(name: "Original Name")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = "Updated Name" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var updatedEntity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(updatedEntity).IsNotNull(); - await Assert.That(updatedEntity!.Name).IsEqualTo("Updated Name"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithUnauthorizedUser_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(name: "Original Name")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = "Updated Name" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Original Name"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_WithNonExistentId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 999, // Non-existent ID - ModuleId = 1, - Name = "Updated Name" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - [Arguments("")] - [Arguments("A")] - [Arguments("Very Long Name That Should Still Work Fine Because We Want To Test Edge Cases")] - public async Task Handle_WithDifferentNames_UpdatesSuccessfully(string newName) - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(name: "Original Name")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = newName - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(1); - - var updatedEntity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(updatedEntity!.Name).IsEqualTo(newName); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Handle_UpdateMultipleTimes_ReflectsLatestChanges() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(name: "Original Name")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - // Act - Update multiple times - await handler.Handle(new UpdateSampleModuleRequest { Id = 1, ModuleId = 1, Name = "First Update" }, CancellationToken.None).ConfigureAwait(false); - await handler.Handle(new UpdateSampleModuleRequest { Id = 1, ModuleId = 1, Name = "Second Update" }, CancellationToken.None).ConfigureAwait(false); - await handler.Handle(new UpdateSampleModuleRequest { Id = 1, ModuleId = 1, Name = "Final Update" }, CancellationToken.None).ConfigureAwait(false); - - // Assert - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Final Update"); - - await connection.CloseAsync().ConfigureAwait(false); - } -} diff --git a/Server.Tests/Features/Security/AuthorizationTests.cs b/Server.Tests/Features/Security/AuthorizationTests.cs deleted file mode 100644 index b791086..0000000 --- a/Server.Tests/Features/Security/AuthorizationTests.cs +++ /dev/null @@ -1,340 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -using static ICTAce.FileHub.Server.Tests.Helpers.SampleModuleTestHelpers; -using CategoryHandlers = ICTAce.FileHub.Features.Categories; -using CategoryHelpers = ICTAce.FileHub.Server.Tests.Helpers.CategoryTestHelpers; - -namespace ICTAce.FileHub.Server.Tests.Features.Security; - -/// -/// Tests for authorization and security features to ensure proper access control. -/// CRITICAL: These tests verify ModuleId isolation and permission enforcement. -/// -public class AuthorizationTests : HandlerTestBase -{ - #region Module Isolation Tests - CRITICAL SECURITY - - [Test] - public async Task Update_WithDifferentModuleId_ReturnsMinusOne() - { - // Arrange - Entity in Module 1, try to update from Module 2 - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 2, // Different module! - Name = "Hacked" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Security check prevented update - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Original"); - await Assert.That(entity.ModuleId).IsEqualTo(1); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Delete_WithDifferentModuleId_ReturnsMinusOne() - { - // Arrange - Entity in Module 1, try to delete from Module 2 - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 2 // Different module! - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Security check prevented delete - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Get_WithDifferentModuleId_ReturnsNull() - { - // Arrange - Entity in Module 1, try to get from Module 2 - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new GetSampleModuleRequest - { - Id = 1, - ModuleId = 2 // Different module! - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Security check prevented access - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_OnlyReturnsEntitiesFromRequestedModule() - { - // Arrange - Entities in both Module 1 and Module 2 - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Module 1 - Item 1"), - CreateTestEntity(id: 2, moduleId: 1, name: "Module 1 - Item 2"), - CreateTestEntity(id: 3, moduleId: 2, name: "Module 2 - Item 1"), - CreateTestEntity(id: 4, moduleId: 2, name: "Module 2 - Item 2")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - Only Module 1 items returned - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(2); - await Assert.That(result.Items.All(x => x.Name.StartsWith("Module 1", StringComparison.Ordinal))).IsTrue(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task CategoryUpdate_WithDifferentModuleId_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await CategoryHelpers.SeedCommandDataAsync(options, - CategoryHelpers.CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new CategoryHandlers.UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: true)); - - var request = new CategoryHandlers.UpdateCategoryRequest - { - Id = 1, - ModuleId = 2, // Different module! - Name = "Hacked", - ViewOrder = 1, - ParentId = 0 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await CategoryHelpers.GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Original"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Permission Tests - - [Test] - public async Task Create_WithViewPermissionOnly_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - var handler = new CreateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new CreateSampleModuleRequest - { - ModuleId = 1, - Name = "Test" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var count = await GetCountFromCommandDbAsync(options).ConfigureAwait(false); - await Assert.That(count).IsEqualTo(0); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Update_WithoutEditPermission_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1, name: "Original")).ConfigureAwait(false); - - var handler = new UpdateHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new UpdateSampleModuleRequest - { - Id = 1, - ModuleId = 1, - Name = "Unauthorized Update" - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity!.Name).IsEqualTo("Original"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Delete_WithoutEditPermission_ReturnsMinusOne() - { - // Arrange - var (connection, options) = await CreateCommandDatabaseAsync().ConfigureAwait(false); - await SeedCommandDataAsync(options, CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new DeleteHandler( - CreateCommandHandlerServices(options, isAuthorized: false)); - - var request = new DeleteSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsEqualTo(-1); - - var entity = await GetFromCommandDbAsync(options, 1).ConfigureAwait(false); - await Assert.That(entity).IsNotNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task Get_WithoutViewPermission_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new GetHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new GetSampleModuleRequest - { - Id = 1, - ModuleId = 1 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - [Test] - public async Task List_WithoutViewPermission_ReturnsNull() - { - // Arrange - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1)).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: false)); - - var request = new ListSampleModuleRequest - { - ModuleId = 1, - PageNumber = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(request, CancellationToken.None).ConfigureAwait(false); - - // Assert - await Assert.That(result).IsNull(); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion - - #region Multi-Module Tests - - [Test] - public async Task MultipleModules_IsolationMaintained() - { - // Arrange - Create entities in different modules - var (connection, options) = await CreateQueryDatabaseAsync().ConfigureAwait(false); - await Helpers.SampleModuleTestHelpers.SeedQueryDataAsync(options, - CreateTestEntity(id: 1, moduleId: 1, name: "Module 1"), - CreateTestEntity(id: 2, moduleId: 2, name: "Module 2"), - CreateTestEntity(id: 3, moduleId: 3, name: "Module 3")).ConfigureAwait(false); - - var handler = new ListHandler( - CreateQueryHandlerServices(options, isAuthorized: true)); - - // Act - Request from Module 2 - var result = await handler.Handle(new ListSampleModuleRequest - { - ModuleId = 2, - PageNumber = 1, - PageSize = 10 - }, CancellationToken.None).ConfigureAwait(false); - - // Assert - Only Module 2 entity returned - var items = result!.Items.ToList(); - await Assert.That(result).IsNotNull(); - await Assert.That(result!.TotalCount).IsEqualTo(1); - await Assert.That(items[0].Name).IsEqualTo("Module 2"); - - await connection.CloseAsync().ConfigureAwait(false); - } - - #endregion -} diff --git a/Server.Tests/GlobalUsings.cs b/Server.Tests/GlobalUsings.cs index 1d5e132..88d3cff 100644 --- a/Server.Tests/GlobalUsings.cs +++ b/Server.Tests/GlobalUsings.cs @@ -1,7 +1,6 @@ // Licensed to ICTAce under the MIT license. global using System.Security.Claims; -global using ICTAce.FileHub.Features.SampleModule; global using ICTAce.FileHub.Persistence; global using ICTAce.FileHub.Server.Tests.Helpers; global using Microsoft.AspNetCore.Http; diff --git a/Server.Tests/Helpers/HandlerTestBase.cs b/Server.Tests/Helpers/HandlerTestBase.cs index 53f5eb7..f3d294e 100644 --- a/Server.Tests/Helpers/HandlerTestBase.cs +++ b/Server.Tests/Helpers/HandlerTestBase.cs @@ -7,7 +7,7 @@ namespace ICTAce.FileHub.Server.Tests.Helpers; /// /// Base class for handler tests providing common test infrastructure. /// Entity-agnostic - handles only databases, mocks, and disposal. -/// Use entity-specific helper classes (SampleModuleTestHelpers, CategoryTestHelpers) for entity operations. +/// Use entity-specific helper classes (CategoryTestHelpers) for entity operations. /// public abstract class HandlerTestBase : IDisposable { diff --git a/Server.Tests/Helpers/SampleModuleTestHelpers.cs b/Server.Tests/Helpers/SampleModuleTestHelpers.cs deleted file mode 100644 index 2f6c297..0000000 --- a/Server.Tests/Helpers/SampleModuleTestHelpers.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Server.Tests.Helpers; - -/// -/// Helper methods for SampleModule entity testing. -/// Provides seeding, creation, and retrieval operations specific to SampleModule. -/// -public static class SampleModuleTestHelpers -{ - #region Seeding Methods - - /// - /// Seeds SampleModule test data into the command context. - /// - public static async Task SeedCommandDataAsync( - DbContextOptions options, - params Persistence.Entities.SampleModule[] entities) - { - using var context = new TestApplicationCommandContext(options); - context.SampleModule.AddRange(entities); - await context.SaveChangesAsync().ConfigureAwait(false); - } - - /// - /// Seeds SampleModule test data into the query context. - /// - public static async Task SeedQueryDataAsync( - DbContextOptions options, - params Persistence.Entities.SampleModule[] entities) - { - using var context = new TestApplicationQueryContext(options); - context.SampleModule.AddRange(entities); - await context.SaveChangesAsync().ConfigureAwait(false); - } - - #endregion - - #region Entity Creation - - /// - /// Creates a test SampleModule entity with default values. - /// - public static Persistence.Entities.SampleModule CreateTestEntity( - int id = 1, - int moduleId = 1, - string name = "Test Module", - string createdBy = "admin", - DateTime? createdOn = null, - string modifiedBy = "admin", - DateTime? modifiedOn = null) - { - return new Persistence.Entities.SampleModule - { - Id = id, - ModuleId = moduleId, - Name = name, - CreatedBy = createdBy, - CreatedOn = createdOn ?? DateTime.UtcNow, - ModifiedBy = modifiedBy, - ModifiedOn = modifiedOn ?? DateTime.UtcNow - }; - } - - #endregion - - #region Retrieval Methods - - /// - /// Gets a SampleModule entity from the command database by ID. - /// - public static async Task GetFromCommandDbAsync( - DbContextOptions options, - int id) - { - using var context = new TestApplicationCommandContext(options); - return await context.SampleModule.FindAsync(id).ConfigureAwait(false); - } - - /// - /// Gets a SampleModule entity from the query database by ID. - /// - public static async Task GetFromQueryDbAsync( - DbContextOptions options, - int id) - { - using var context = new TestApplicationQueryContext(options); - return await context.SampleModule.FindAsync(id).ConfigureAwait(false); - } - - /// - /// Gets the count of SampleModule entities in the command database. - /// - public static async Task GetCountFromCommandDbAsync( - DbContextOptions options) - { - using var context = new TestApplicationCommandContext(options); - return await context.SampleModule.CountAsync().ConfigureAwait(false); - } - - /// - /// Gets the count of SampleModule entities in the query database. - /// - public static async Task GetCountFromQueryDbAsync( - DbContextOptions options) - { - using var context = new TestApplicationQueryContext(options); - return await context.SampleModule.CountAsync().ConfigureAwait(false); - } - - #endregion -} diff --git a/Server.Tests/Helpers/TestDbContexts.cs b/Server.Tests/Helpers/TestDbContexts.cs index 57c5b0d..4936b83 100644 --- a/Server.Tests/Helpers/TestDbContexts.cs +++ b/Server.Tests/Helpers/TestDbContexts.cs @@ -108,7 +108,6 @@ protected override void OnModelCreating(ModelBuilder builder) // Instead, manually configure Identity entities // Configure our entities with simple table names (no tenant rewriting for tests) - builder.Entity().ToTable("Company_SampleModule"); builder.Entity().ToTable("ICTAce_FileHub_Category"); // Ignore Identity entities that we don't need for these tests @@ -187,7 +186,6 @@ protected override void OnModelCreating(ModelBuilder builder) // Instead, manually configure Identity entities // Configure our entities with simple table names (no tenant rewriting for tests) - builder.Entity().ToTable("Company_SampleModule"); builder.Entity().ToTable("ICTAce_FileHub_Category"); // Ignore Identity entities that we don't need for these tests diff --git a/Server.Tests/ICTAce.FileHub.Server.Tests.csproj b/Server.Tests/ICTAce.FileHub.Server.Tests.csproj index bf2c79d..fb044a4 100644 --- a/Server.Tests/ICTAce.FileHub.Server.Tests.csproj +++ b/Server.Tests/ICTAce.FileHub.Server.Tests.csproj @@ -13,4 +13,10 @@ + + + + + + diff --git a/Server/Features/Files/Controller.cs b/Server/Features/Files/Controller.cs new file mode 100644 index 0000000..fc7e783 --- /dev/null +++ b/Server/Features/Files/Controller.cs @@ -0,0 +1,392 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +[Route("api/ictace/fileHub/files")] +[ApiController] +public class ICTAceFileHubFilesController( + IMediator mediator, + ILogManager logger, + IHttpContextAccessor accessor, + IWebHostEnvironment environment, + ITenantManager tenantManager) + : ModuleControllerBase(logger, accessor) +{ + private readonly IMediator _mediator = mediator; + private readonly IWebHostEnvironment _environment = environment; + private readonly ITenantManager _tenantManager = tenantManager; + + [HttpGet("{id}")] + [Authorize(Policy = PolicyNames.ViewModule)] + [ProducesResponseType(typeof(GetFileDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetAsync( + int id, + [FromQuery] int moduleId, + CancellationToken cancellationToken = default) + { + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized FileHub File Get Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); + return Forbid(); + } + + if (id <= 0) + { + return BadRequest("Invalid File ID"); + } + + var query = new GetFileRequest + { + ModuleId = moduleId, + Id = id, + }; + + var file = await _mediator.Send(query, cancellationToken).ConfigureAwait(false); + + if (file is null) + { + _logger.Log(LogLevel.Warning, this, LogFunction.Read, + "File Not Found Id={Id} in ModuleId={ModuleId}", id, moduleId); + return NotFound(); + } + + return Ok(file); + } + + [HttpGet("")] + [Authorize(Policy = PolicyNames.ViewModule)] + [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> ListAsync( + [FromQuery] int moduleId, + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10, + CancellationToken cancellationToken = default) + { + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized File List Attempt ModuleId={ModuleId}", moduleId); + return Forbid(); + } + + if (pageSize > 100) + { + pageSize = 100; + } + + if (pageNumber < 1) + { + pageNumber = 1; + } + + var query = new ListFileRequest + { + ModuleId = moduleId, + PageNumber = pageNumber, + PageSize = pageSize, + }; + + var result = await _mediator.Send(query, cancellationToken).ConfigureAwait(false); + + if (result is null) + { + return NotFound(); + } + + return Ok(result); + } + + [HttpPost("")] + [Authorize(Policy = PolicyNames.EditModule)] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> CreateAsync( + [FromQuery] int moduleId, + [FromBody] CreateAndUpdateFileDto dto, + CancellationToken cancellationToken = default) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized File Create Attempt ModuleId={ModuleId}", moduleId); + return Forbid(); + } + + var command = new CreateFileRequest + { + ModuleId = moduleId, + Name = dto.Name, + FileName = dto.FileName, + ImageName = dto.ImageName, + Description = dto.Description, + FileSize = dto.FileSize, + Downloads = dto.Downloads, + CategoryIds = dto.CategoryIds + }; + + var id = await _mediator.Send(command, cancellationToken).ConfigureAwait(false); + + return Created( + Url.Action(nameof(GetAsync), new { id, moduleId = command.ModuleId }) ?? string.Empty, + id); + } + + [HttpPut("{id}")] + [Authorize(Policy = PolicyNames.EditModule)] + [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> UpdateAsync( + int id, + [FromQuery] int moduleId, + [FromBody] CreateAndUpdateFileDto dto, + CancellationToken cancellationToken = default) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized File Update Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); + return Forbid(); + } + + var command = new UpdateFileRequest + { + Id = id, + ModuleId = moduleId, + Name = dto.Name, + FileName = dto.FileName, + ImageName = dto.ImageName, + Description = dto.Description, + FileSize = dto.FileSize, + CategoryIds = dto.CategoryIds + }; + + var result = await _mediator.Send(command, cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + + [HttpDelete("{id}")] + [Authorize(Policy = PolicyNames.EditModule)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteAsync( + int id, + [FromQuery] int moduleId, + CancellationToken cancellationToken = default) + { + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized File Delete Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); + return Forbid(); + } + + if (id <= 0) + { + return BadRequest("Invalid File ID"); + } + + var command = new DeleteFileRequest + { + ModuleId = moduleId, + Id = id, + }; + + await _mediator.Send(command, cancellationToken).ConfigureAwait(false); + + return NoContent(); + } + + [HttpPost("upload")] + [Authorize(Policy = PolicyNames.EditModule)] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> UploadFileAsync( + [FromQuery] int moduleId, + IFormFile file, + CancellationToken cancellationToken = default) + { + if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, + "Unauthorized File Upload Attempt ModuleId={ModuleId}", moduleId); + return Forbid(); + } + + if (file is null || file.Length == 0) + { + return BadRequest("No file uploaded"); + } + + try + { + var alias = _tenantManager.GetAlias(); + var filePath = GetFileStoragePath(alias.TenantId, alias.SiteId, moduleId); + + // Ensure directory exists + if (!Directory.Exists(filePath)) + { + Directory.CreateDirectory(filePath); + } + + // Generate unique filename to prevent overwrites + var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}"; + var fullPath = Path.Combine(filePath, fileName); + + // Save the file + using (var stream = new FileStream(fullPath, FileMode.Create)) + { + await file.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); + } + + _logger.Log(LogLevel.Information, this, LogFunction.Create, + "File Uploaded FileName={FileName} Size={Size} ModuleId={ModuleId}", + fileName, file.Length, moduleId); + + return Ok(fileName); + } + catch (Exception ex) + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, + ex, "Error Uploading File ModuleId={ModuleId}", moduleId); + return StatusCode(StatusCodes.Status500InternalServerError, "Error uploading file"); + } + } + + [HttpGet("serve/{fileName}")] + [AllowAnonymous] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ServeFileAsync( + string fileName, + [FromQuery] bool download = false, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(fileName)) + { + _logger.Log(LogLevel.Warning, this, LogFunction.Read, + "ServeFile: Empty filename provided"); + return BadRequest("Filename is required"); + } + + try + { + _logger.Log(LogLevel.Information, this, LogFunction.Read, + "ServeFile: Looking up file FileName={FileName} Download={Download}", fileName, download); + + var fileModuleInfo = await _mediator.Send( + new GetFileByFileNameRequest { FileName = fileName }, + cancellationToken).ConfigureAwait(false); + + if (fileModuleInfo is null) + { + _logger.Log(LogLevel.Warning, this, LogFunction.Read, + "ServeFile: File not found in database FileName={FileName}", fileName); + return NotFound(new { message = "File not found in database", fileName }); + } + + _logger.Log(LogLevel.Information, this, LogFunction.Read, + "ServeFile: File found in database ModuleId={ModuleId} FileId={FileId} FileName={FileName}", + fileModuleInfo.ModuleId, fileModuleInfo.FileId, fileName); + + var alias = _tenantManager.GetAlias(); + var filePath = GetFileStoragePath(alias.TenantId, alias.SiteId, fileModuleInfo.ModuleId); + var fullPath = Path.Combine(filePath, fileName); + + _logger.Log(LogLevel.Information, this, LogFunction.Read, + "ServeFile: Checking physical file path Path={Path}", fullPath); + + if (!System.IO.File.Exists(fullPath)) + { + _logger.Log(LogLevel.Warning, this, LogFunction.Read, + "ServeFile: Physical file not found Path={Path}", fullPath); + return NotFound(new { message = "Physical file not found", path = fullPath }); + } + + // Only increment download counter if this is an actual download (not image display) + if (download) + { + await _mediator.Send( + new IncrementDownloadRequest { FileId = fileModuleInfo.FileId, ModuleId = fileModuleInfo.ModuleId }, + cancellationToken).ConfigureAwait(false); + } + + var contentType = GetContentType(fileName); + var fileStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + _logger.Log(LogLevel.Information, this, LogFunction.Read, + "ServeFile: Successfully serving file FileName={FileName} ModuleId={ModuleId} ContentType={ContentType} Download={Download}", + fileName, fileModuleInfo.ModuleId, contentType, download); + + return File(fileStream, contentType, Path.GetFileName(fileName)); + } + catch (Exception ex) + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, + ex, "ServeFile: Error serving file FileName={FileName} Error={Error}", fileName, ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, + new { message = "Error serving file", error = ex.Message }); + } + } + + private string GetFileStoragePath(int tenantId, int siteId, int moduleId) + { + // Content/Tenants/{TenantId}/Sites/{SiteId}/FileHub/{ModuleId}/ + return Path.Combine( + _environment.ContentRootPath, + "Content", + "Tenants", + tenantId.ToString(System.Globalization.CultureInfo.InvariantCulture), + "Sites", + siteId.ToString(System.Globalization.CultureInfo.InvariantCulture), + "FileHub", + moduleId.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + + private static string GetContentType(string fileName) + { + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + return extension switch + { + ".pdf" => "application/pdf", + ".doc" => "application/msword", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".xls" => "application/vnd.ms-excel", + ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".ppt" => "application/vnd.ms-powerpoint", + ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".zip" => "application/zip", + ".rar" => "application/x-rar-compressed", + ".txt" => "text/plain", + ".csv" => "text/csv", + ".json" => "application/json", + ".xml" => "application/xml", + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".bmp" => "image/bmp", + ".svg" => "image/svg+xml", + ".mp3" => "audio/mpeg", + ".mp4" => "video/mp4", + ".avi" => "video/x-msvideo", + _ => "application/octet-stream" + }; + } +} diff --git a/Server/Features/Files/Create.cs b/Server/Features/Files/Create.cs new file mode 100644 index 0000000..d699e9c --- /dev/null +++ b/Server/Features/Files/Create.cs @@ -0,0 +1,64 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record CreateFileRequest : RequestBase, IRequest +{ + public string Name { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string ImageName { get; set; } = string.Empty; + public string? Description { get; set; } + public string FileSize { get; set; } = string.Empty; + public int Downloads { get; set; } + public List CategoryIds { get; set; } = []; +} + +public class CreateHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + private static readonly CreateMapper _mapper = new(); + + public async Task Handle(CreateFileRequest request, CancellationToken cancellationToken) + { + var alias = GetAlias(); + + if (!IsAuthorized(alias.SiteId, request.ModuleId, PermissionNames.Edit)) + { + Logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Add Attempt {ModuleId}", request.ModuleId); + return -1; + } + + var entity = _mapper.ToEntity(request); + + using var db = CreateDbContext(); + db.Set().Add(entity); + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + // Save file-category relationships + if (request.CategoryIds.Any()) + { + foreach (var categoryId in request.CategoryIds) + { + var fileCategory = new Persistence.Entities.FileCategory + { + FileId = entity.Id, + CategoryId = categoryId, + ModuleId = entity.ModuleId, + CreatedBy = entity.CreatedBy, + CreatedOn = entity.CreatedOn + }; + db.Set().Add(fileCategory); + } + var result = await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + Logger.Log(LogLevel.Information, this, LogFunction.Create, "File Added {Entity}", entity); + return entity.Id; + } +} + +[Mapper] +internal sealed partial class CreateMapper +{ + internal partial ICTAce.FileHub.Persistence.Entities.File ToEntity(CreateFileRequest request); +} diff --git a/Server/Features/Files/Delete.cs b/Server/Features/Files/Delete.cs new file mode 100644 index 0000000..458b0b3 --- /dev/null +++ b/Server/Features/Files/Delete.cs @@ -0,0 +1,17 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record DeleteFileRequest : EntityRequestBase, IRequest; + +public class DeleteHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + public Task Handle(DeleteFileRequest request, CancellationToken cancellationToken) + { + return HandleDeleteAsync( + request: request, + cancellationToken: cancellationToken + ); + } +} diff --git a/Server/Features/Files/Get.cs b/Server/Features/Files/Get.cs new file mode 100644 index 0000000..47580d3 --- /dev/null +++ b/Server/Features/Files/Get.cs @@ -0,0 +1,63 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record GetFileRequest : EntityRequestBase, IRequest; + +public record GetFileDto +{ + public int Id { get; set; } + public int ModuleId { get; set; } + public required string Name { get; set; } + public required string FileName { get; set; } + public required string ImageName { get; set; } + public string? Description { get; set; } + public required string FileSize { get; set; } + public int Downloads { get; set; } + public List CategoryIds { get; set; } = []; + + public required string CreatedBy { get; set; } + public required DateTime CreatedOn { get; set; } + public required string ModifiedBy { get; set; } + public required DateTime ModifiedOn { get; set; } +} + +public class GetHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + private static readonly GetMapper _mapper = new(); + + public async Task Handle(GetFileRequest request, CancellationToken cancellationToken) + { + var alias = GetAlias(); + + if (!IsAuthorized(alias.SiteId, request.ModuleId, PermissionNames.View)) + { + Logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {Id} {ModuleId}", request.Id, request.ModuleId); + return null; + } + + using var db = CreateDbContext(); + var entity = await db.Set() + .Include(f => f.FileCategories) + .SingleOrDefaultAsync(e => e.Id == request.Id && e.ModuleId == request.ModuleId, cancellationToken) + .ConfigureAwait(false); + + if (entity is null) + { + Logger.Log(LogLevel.Error, this, LogFunction.Read, "File not found {Id} {ModuleId}", request.Id, request.ModuleId); + return null; + } + + var dto = _mapper.ToDto(entity); + dto.CategoryIds = entity.FileCategories.Select(fc => fc.CategoryId).ToList(); + + return dto; + } +} + +[Mapper] +internal sealed partial class GetMapper +{ + internal partial GetFileDto ToDto(Persistence.Entities.File entity); +} diff --git a/Server/Features/Files/GetByFileName.cs b/Server/Features/Files/GetByFileName.cs new file mode 100644 index 0000000..5f4901d --- /dev/null +++ b/Server/Features/Files/GetByFileName.cs @@ -0,0 +1,47 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record GetFileByFileNameRequest : IRequest +{ + public required string FileName { get; set; } +} + +public record FileModuleInfo +{ + public int FileId { get; set; } + public int ModuleId { get; set; } +} + +public class GetByFileNameHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + public async Task Handle(GetFileByFileNameRequest request, CancellationToken cancellationToken) + { + using var db = CreateDbContext(); + + var trimmedFileName = request.FileName.Trim(); + + // Look up file by FileName or ImageName (case-insensitive) + var file = await db.File + .Where(f => f.FileName == trimmedFileName || f.ImageName == trimmedFileName) + .Select(f => new FileModuleInfo { FileId = f.Id, ModuleId = f.ModuleId }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + // Log if not found for debugging + if (file is null) + { + Logger.Log(LogLevel.Warning, this, LogFunction.Read, + "GetByFileName: File not found in database FileName={FileName}", trimmedFileName); + } + else + { + Logger.Log(LogLevel.Information, this, LogFunction.Read, + "GetByFileName: File found FileId={FileId} ModuleId={ModuleId} FileName={FileName}", + file.FileId, file.ModuleId, trimmedFileName); + } + + return file; + } +} diff --git a/Server/Features/Files/IncrementDownload.cs b/Server/Features/Files/IncrementDownload.cs new file mode 100644 index 0000000..3c5f6da --- /dev/null +++ b/Server/Features/Files/IncrementDownload.cs @@ -0,0 +1,38 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record IncrementDownloadRequest : IRequest +{ + public int FileId { get; set; } + public int ModuleId { get; set; } +} + +public class IncrementDownloadHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + public async Task Handle(IncrementDownloadRequest request, CancellationToken cancellationToken) + { + using var db = CreateDbContext(); + + var rowsAffected = await db.File + .Where(f => f.Id == request.FileId && f.ModuleId == request.ModuleId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(f => f.Downloads, f => f.Downloads + 1), + cancellationToken) + .ConfigureAwait(false); + + if (rowsAffected > 0) + { + Logger.Log(LogLevel.Information, this, LogFunction.Update, + "Download counter incremented FileId={FileId} ModuleId={ModuleId}", + request.FileId, request.ModuleId); + return request.FileId; + } + + Logger.Log(LogLevel.Warning, this, LogFunction.Update, + "Failed to increment download counter FileId={FileId} ModuleId={ModuleId}", + request.FileId, request.ModuleId); + return -1; + } +} diff --git a/Server/Features/Files/List.cs b/Server/Features/Files/List.cs new file mode 100644 index 0000000..40310e5 --- /dev/null +++ b/Server/Features/Files/List.cs @@ -0,0 +1,27 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record ListFileRequest : PagedRequestBase, IRequest>; + +public class ListHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler?> +{ + private static readonly ListMapper _mapper = new(); + + public Task?> Handle(ListFileRequest request, CancellationToken cancellationToken) + { + return HandleListAsync( + request: request, + mapToResponse: _mapper.ToListResponse, + orderBy: query => query.OrderBy(f => f.Name), + cancellationToken: cancellationToken + ); + } +} + +[Mapper] +internal sealed partial class ListMapper +{ + public partial ListFileDto ToListResponse(ICTAce.FileHub.Persistence.Entities.File file); +} diff --git a/Server/Features/Files/Update.cs b/Server/Features/Files/Update.cs new file mode 100644 index 0000000..fc1cc08 --- /dev/null +++ b/Server/Features/Files/Update.cs @@ -0,0 +1,73 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Features.Files; + +public record UpdateFileRequest : EntityRequestBase, IRequest +{ + public required string Name { get; set; } + public required string FileName { get; set; } + public required string ImageName { get; set; } + public string? Description { get; set; } + public required string FileSize { get; set; } + public List CategoryIds { get; set; } = []; +} + +public class UpdateHandler(HandlerServices services) + : HandlerBase(services), IRequestHandler +{ + public async Task Handle(UpdateFileRequest request, CancellationToken cancellationToken) + { + var alias = GetAlias(); + + if (!IsAuthorized(alias.SiteId, request.ModuleId, PermissionNames.Edit)) + { + Logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Update Attempt {Id}", request.Id); + return -1; + } + + using var db = CreateDbContext(); + + // Update file properties (excluding Downloads - managed by system via IncrementDownload) + var rowsAffected = await db.Set() + .Where(e => e.Id == request.Id && e.ModuleId == request.ModuleId) + .ExecuteUpdateAsync(setter => setter + .SetProperty(e => e.Name, request.Name) + .SetProperty(e => e.FileName, request.FileName) + .SetProperty(e => e.ImageName, request.ImageName) + .SetProperty(e => e.Description, request.Description) + .SetProperty(e => e.FileSize, request.FileSize), + cancellationToken) + .ConfigureAwait(false); + + if (rowsAffected > 0) + { + // Update file-category relationships + // First, remove existing relationships + await db.Set() + .Where(fc => fc.FileId == request.Id) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + + // Then, add new relationships + if (request.CategoryIds.Any()) + { + foreach (var categoryId in request.CategoryIds) + { + var fileCategory = new Persistence.Entities.FileCategory + { + FileId = request.Id, + CategoryId = categoryId + }; + db.Set().Add(fileCategory); + } + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + Logger.Log(LogLevel.Information, this, LogFunction.Update, "File Updated {Id}", request.Id); + return request.Id; + } + + Logger.Log(LogLevel.Warning, this, LogFunction.Update, "File Not Found {Id}", request.Id); + return -1; + } +} diff --git a/Server/Features/SampleModule/Controller.cs b/Server/Features/SampleModule/Controller.cs deleted file mode 100644 index 59a22e0..0000000 --- a/Server/Features/SampleModule/Controller.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -[Route("api/company/sampleModules")] -[ApiController] -public class CompanySampleModulesController( - IMediator mediator, - ILogManager logger, - IHttpContextAccessor accessor) - : ModuleControllerBase(logger, accessor) -{ - private readonly IMediator _mediator = mediator; - - [HttpGet("{id}")] - [Authorize(Policy = PolicyNames.ViewModule)] - [ProducesResponseType(typeof(GetSampleModuleDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetAsync( - int id, - [FromQuery] int moduleId, - CancellationToken cancellationToken = default) - { - if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, - "Unauthorized SampleModule Get Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); - return Forbid(); - } - - if (id <= 0) - { - return BadRequest("Invalid SampleModule ID"); - } - - var query = new GetSampleModuleRequest - { - ModuleId = moduleId, - Id = id, - }; - - var sampleModule = await _mediator.Send(query, cancellationToken).ConfigureAwait(false); - - if (sampleModule is null) - { - _logger.Log(LogLevel.Warning, this, LogFunction.Read, - "SampleModule Not Found Id={Id} in ModuleId={ModuleId}", id, moduleId); - return NotFound(); - } - - return Ok(sampleModule); - } - - [HttpGet("")] - [Authorize(Policy = PolicyNames.ViewModule)] - [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>> ListAsync( - [FromQuery] int moduleId, - [FromQuery] int pageNumber = 1, - [FromQuery] int pageSize = 10, - CancellationToken cancellationToken = default) - { - if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, - "Unauthorized SampleModule List Attempt ModuleId={ModuleId}", moduleId); - return Forbid(); - } - - if (pageSize > 100) - { - pageSize = 100; - } - - if (pageNumber < 1) - { - pageNumber = 1; - } - - var query = new ListSampleModuleRequest - { - ModuleId = moduleId, - PageNumber = pageNumber, - PageSize = pageSize, - }; - - var result = await _mediator.Send(query, cancellationToken).ConfigureAwait(false); - - if (result is null) - { - return NotFound(); - } - - return Ok(result); - } - - [HttpPost("")] - [Authorize(Policy = PolicyNames.EditModule)] - [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> CreateAsync( - [FromQuery] int moduleId, - [FromBody] CreateAndUpdateSampleModuleDto dto, - CancellationToken cancellationToken = default) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, - "Unauthorized SampleModule Create Attempt ModuleId={ModuleId}", moduleId); - return Forbid(); - } - - var request = new CreateSampleModuleRequest - { - ModuleId = moduleId, - Name = dto.Name, - }; - - var id = await _mediator.Send(request, cancellationToken).ConfigureAwait(false); - - return Created( - Url.Action(nameof(GetAsync), new { id, moduleId = request.ModuleId }) ?? string.Empty, - id); - } - - [HttpPut("{id}")] - [Authorize(Policy = PolicyNames.EditModule)] - [ProducesResponseType(typeof(int), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> UpdateAsync( - int id, - [FromQuery] int moduleId, - [FromBody] CreateAndUpdateSampleModuleDto dto, - CancellationToken cancellationToken = default) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - - if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, - "Unauthorized SampleModule Update Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); - return Forbid(); - } - - var request = new UpdateSampleModuleRequest - { - Id = id, - ModuleId = moduleId, - Name = dto.Name, - }; - - var result = await _mediator.Send(request, cancellationToken).ConfigureAwait(false); - - return Ok(result); - } - - [HttpDelete("{id}")] - [Authorize(Policy = PolicyNames.EditModule)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task DeleteAsync( - int id, - [FromQuery] int moduleId, - CancellationToken cancellationToken = default) - { - if (!IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, - "Unauthorized SampleModule Delete Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId); - return Forbid(); - } - - if (id <= 0) - { - return BadRequest("Invalid SampleModule ID"); - } - - var command = new DeleteSampleModuleRequest - { - ModuleId = moduleId, - Id = id, - }; - - await _mediator.Send(command, cancellationToken).ConfigureAwait(false); - - return NoContent(); - } -} diff --git a/Server/Features/SampleModule/Create.cs b/Server/Features/SampleModule/Create.cs deleted file mode 100644 index 0875f36..0000000 --- a/Server/Features/SampleModule/Create.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -public record CreateSampleModuleRequest : RequestBase, IRequest -{ - public string Name { get; set; } = string.Empty; -} - -public class CreateHandler(HandlerServices services) - : HandlerBase(services), IRequestHandler -{ - private static readonly CreateMapper _mapper = new(); - - public Task Handle(CreateSampleModuleRequest request, CancellationToken cancellationToken) - { - return HandleCreateAsync( - request: request, - mapToEntity: _mapper.ToEntity, - cancellationToken: cancellationToken - ); - } -} - -[Mapper] -internal sealed partial class CreateMapper -{ - internal partial Persistence.Entities.SampleModule ToEntity(CreateSampleModuleRequest request); -} diff --git a/Server/Features/SampleModule/Delete.cs b/Server/Features/SampleModule/Delete.cs deleted file mode 100644 index ed6b049..0000000 --- a/Server/Features/SampleModule/Delete.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -public record DeleteSampleModuleRequest : EntityRequestBase, IRequest; - -public class DeleteHandler(HandlerServices services) - : HandlerBase(services), IRequestHandler -{ - public Task Handle(DeleteSampleModuleRequest request, CancellationToken cancellationToken) - { - return HandleDeleteAsync( - request: request, - cancellationToken: cancellationToken - ); - } -} diff --git a/Server/Features/SampleModule/Get.cs b/Server/Features/SampleModule/Get.cs deleted file mode 100644 index 8105468..0000000 --- a/Server/Features/SampleModule/Get.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -public record GetSampleModuleRequest : EntityRequestBase, IRequest; - -public class GetHandler(HandlerServices services) - : HandlerBase(services), IRequestHandler -{ - private static readonly GetMapper _mapper = new(); - - public Task Handle(GetSampleModuleRequest request, CancellationToken cancellationToken) - { - return HandleGetAsync( - request: request, - mapToResponse: _mapper.ToGetResponse, - cancellationToken: cancellationToken - ); - } -} - -[Mapper] -internal sealed partial class GetMapper -{ - /// - /// Maps SampleModule entity to GetSampleModuleResponse DTO - /// - public partial GetSampleModuleDto ToGetResponse(Persistence.Entities.SampleModule sampleModule); -} diff --git a/Server/Features/SampleModule/List.cs b/Server/Features/SampleModule/List.cs deleted file mode 100644 index fd5f6f3..0000000 --- a/Server/Features/SampleModule/List.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -public record ListSampleModuleRequest : PagedRequestBase, IRequest>; - -public class ListHandler(HandlerServices services) - : HandlerBase(services), IRequestHandler?> -{ - private static readonly ListMapper _mapper = new(); - - public Task?> Handle(ListSampleModuleRequest request, CancellationToken cancellationToken) - { - return HandleListAsync( - request: request, - mapToResponse: _mapper.ToListResponse, - orderBy: query => query.OrderBy(m => m.Name), - cancellationToken: cancellationToken - ); - } -} - -[Mapper] -internal sealed partial class ListMapper -{ - public partial ListSampleModuleDto ToListResponse(Persistence.Entities.SampleModule sampleModule); -} diff --git a/Server/Features/SampleModule/Update.cs b/Server/Features/SampleModule/Update.cs deleted file mode 100644 index ac6495a..0000000 --- a/Server/Features/SampleModule/Update.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Features.SampleModule; - -public record UpdateSampleModuleRequest : EntityRequestBase, IRequest -{ - public required string Name { get; set; } -} - -public class UpdateHandler(HandlerServices services) - : HandlerBase(services), IRequestHandler -{ - public Task Handle(UpdateSampleModuleRequest request, CancellationToken cancellationToken) - { - return HandleUpdateAsync( - request: request, - setPropertyCalls: setter => setter.SetProperty(e => e.Name, request.Name), - cancellationToken: cancellationToken - ); - } -} diff --git a/Server/Managers/SampleModule.cs b/Server/Managers/FileHub.cs similarity index 60% rename from Server/Managers/SampleModule.cs rename to Server/Managers/FileHub.cs index 7b773c5..3a5cc00 100644 --- a/Server/Managers/SampleModule.cs +++ b/Server/Managers/FileHub.cs @@ -2,7 +2,7 @@ namespace ICTAce.FileHub.Managers; -public class SampleModule( +public class FileHub( IDbContextFactory contextFactory, IDBContextDependencies DBContextDependencies) : MigratableModuleBase, IInstallable, IPortable, ISearchable @@ -26,32 +26,41 @@ public string ExportModule(Module module) // Direct data access - no repository layer using var db = _contextFactory.CreateDbContext(); - var sampleModule = db.SampleModule + var files = db.File .Where(item => item.ModuleId == module.ModuleId) .ToList(); - if (sampleModule != null) + if (files.Count > 0) { - content = JsonSerializer.Serialize(sampleModule); + content = JsonSerializer.Serialize(files); } return content; } public void ImportModule(Module module, string content, string version) { - List SampleModules = null; + List? files = null; if (!string.IsNullOrEmpty(content)) { - SampleModules = JsonSerializer.Deserialize>(content); + files = JsonSerializer.Deserialize>(content); } - if (SampleModules is not null) + if (files is not null) { // Direct data access - no repository layer using var db = _contextFactory.CreateDbContext(); - foreach (var task in SampleModules) + foreach (var file in files) { - db.SampleModule.Add(new Persistence.Entities.SampleModule { ModuleId = module.ModuleId, Name = task.Name }); + db.File.Add(new Persistence.Entities.File + { + ModuleId = module.ModuleId, + Name = file.Name, + FileName = file.FileName, + ImageName = file.ImageName, + Description = file.Description, + FileSize = file.FileSize, + Downloads = file.Downloads + }); } db.SaveChanges(); } @@ -63,18 +72,18 @@ public Task> GetSearchContentsAsync(PageModule pageModule, D // Direct data access - no repository layer using var db = _contextFactory.CreateDbContext(); - foreach (var sampleModule in db.SampleModule.Where(item => item.ModuleId == pageModule.ModuleId)) + foreach (var file in db.File.Where(item => item.ModuleId == pageModule.ModuleId)) { - if (sampleModule.ModifiedOn >= lastIndexedOn) + if (file.ModifiedOn >= lastIndexedOn) { searchContentList.Add(new SearchContent { - EntityName = "Company_SampleModule", - EntityId = sampleModule.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), - Title = sampleModule.Name, - Body = sampleModule.Name, - ContentModifiedBy = sampleModule.ModifiedBy, - ContentModifiedOn = sampleModule.ModifiedOn + EntityName = "ICTAce_FileHub_File", + EntityId = file.Id.ToString(System.Globalization.CultureInfo.InvariantCulture), + Title = file.Name, + Body = file.Description ?? string.Empty, + ContentModifiedBy = file.ModifiedBy, + ContentModifiedOn = file.ModifiedOn }); } } diff --git a/Server/Persistence/ApplicationContext.cs b/Server/Persistence/ApplicationContext.cs index ff4af51..86da12c 100644 --- a/Server/Persistence/ApplicationContext.cs +++ b/Server/Persistence/ApplicationContext.cs @@ -6,15 +6,14 @@ public class ApplicationContext( IDBContextDependencies DBContextDependencies) : DBContextBase(DBContextDependencies), ITransientService, IMultiDatabase { - public virtual DbSet SampleModule { get; set; } public virtual DbSet Category { get; set; } + public virtual DbSet File { get; set; } + public virtual DbSet FileCategory { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - builder.Entity().ToTable(ActiveDatabase.RewriteName("Company_SampleModule")); - builder.Entity(entity => { entity.ToTable(ActiveDatabase.RewriteName("ICTAce_FileHub_Category")); @@ -24,5 +23,22 @@ protected override void OnModelCreating(ModelBuilder builder) .HasForeignKey(c => c.ParentId) .OnDelete(DeleteBehavior.Restrict); }); + + builder.Entity().ToTable(ActiveDatabase.RewriteName("ICTAce_FileHub_File")); + + builder.Entity(entity => + { + entity.ToTable(ActiveDatabase.RewriteName("ICTAce_FileHub_FileCategory")); + + entity.HasOne(fc => fc.FileHub) + .WithMany(f => f.FileCategories) + .HasForeignKey(fc => fc.FileId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(fc => fc.Category) + .WithMany(c => c.FileCategories) + .HasForeignKey(fc => fc.CategoryId) + .OnDelete(DeleteBehavior.Restrict); + }); } } diff --git a/Server/Persistence/ApplicationQueryContext.cs b/Server/Persistence/ApplicationQueryContext.cs index 536367a..f9f0e4b 100644 --- a/Server/Persistence/ApplicationQueryContext.cs +++ b/Server/Persistence/ApplicationQueryContext.cs @@ -14,6 +14,13 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) if (tenant != null) { var queryConnectionString = DBContextDependencies.Config.GetConnectionString(tenant.DBConnectionString + "_Query"); + + // If query connection string is not available, fall back to regular connection string + if (string.IsNullOrEmpty(queryConnectionString)) + { + queryConnectionString = DBContextDependencies.Config.GetConnectionString(tenant.DBConnectionString); + } + if (!string.IsNullOrEmpty(queryConnectionString)) { queryConnectionString = queryConnectionString.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString()); diff --git a/Server/Persistence/Entities/Category.cs b/Server/Persistence/Entities/Category.cs index 2d16070..5c4b7eb 100644 --- a/Server/Persistence/Entities/Category.cs +++ b/Server/Persistence/Entities/Category.cs @@ -2,12 +2,6 @@ namespace ICTAce.FileHub.Persistence.Entities; -/// -/// Represents a category that can be organized hierarchically and ordered for display purposes. -/// -/// A category may have a parent category, allowing for the creation of nested category structures. The -/// display order of categories can be controlled using the ViewOrder property. Inherits auditing properties from -/// AuditableModuleBase. public class Category : AuditableModuleBase { [MaxLength(100)] @@ -20,4 +14,6 @@ public class Category : AuditableModuleBase public Category? ParentCategory { get; set; } public ICollection? Subcategories { get; set; } + + public ICollection FileCategories { get; set; } = []; } diff --git a/Server/Persistence/Entities/File.cs b/Server/Persistence/Entities/File.cs new file mode 100644 index 0000000..5c2ffb1 --- /dev/null +++ b/Server/Persistence/Entities/File.cs @@ -0,0 +1,26 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Persistence.Entities; + +public class File : AuditableModuleBase +{ + [Required] + [MaxLength(100)] + public required string Name { get; set; } + + [MaxLength(255)] + public required string FileName { get; set; } + + [MaxLength(255)] + public required string ImageName { get; set; } + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(12)] + public required string FileSize { get; set; } + + public int Downloads { get; set; } + + public ICollection FileCategories { get; set; } = []; +} diff --git a/Server/Persistence/Entities/FileCategory.cs b/Server/Persistence/Entities/FileCategory.cs new file mode 100644 index 0000000..6d1b0d4 --- /dev/null +++ b/Server/Persistence/Entities/FileCategory.cs @@ -0,0 +1,15 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Persistence.Entities; + +public class FileCategory : AuditableModuleBase +{ + [Key] + public int Id { get; set; } + + public int FileId { get; set; } + public File FileHub { get; set; } = null!; + + public int CategoryId { get; set; } + public Category Category { get; set; } = null!; +} diff --git a/Server/Persistence/Entities/FileHub.cs b/Server/Persistence/Entities/FileHub.cs deleted file mode 100644 index 310e399..0000000 --- a/Server/Persistence/Entities/FileHub.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Persistence.Entities; - -/// -/// Represents a file entry with associated metadata, including name, file name, description, file size, and download -/// count. -/// -/// This class is typically used to store and manage information about files within the application, such -/// as for file repositories or download modules. It includes auditing information inherited from the -/// AuditableModuleBase class. -public class FileHub : AuditableModuleBase -{ - [Required] - public required string Name { get; set; } - - [MaxLength(255)] - public required string FileName { get; set; } - - [MaxLength(1000)] - public string? Description { get; set; } - - [MaxLength(12)] - public required string FileSize { get; set; } - - public int Downloads { get; set; } -} diff --git a/Server/Persistence/Entities/FileHubCategory.cs b/Server/Persistence/Entities/FileHubCategory.cs deleted file mode 100644 index fa05c27..0000000 --- a/Server/Persistence/Entities/FileHubCategory.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Persistence.Entities; - -/// -/// Represents the association between a file hub and a category. -/// -/// This class is typically used to model a many-to-many relationship between file hubs and categories -/// within the data model. Each instance links a specific file hub to a specific category. -public class FileHubCategory -{ - [Key] - public int Id { get; set; } - - public int FileHubId { get; set; } - public FileHub FileHub { get; set; } = null!; - - public int CategoryId { get; set; } - public Category Category { get; set; } = null!; -} diff --git a/Server/Persistence/Entities/SampleModule.cs b/Server/Persistence/Entities/SampleModule.cs deleted file mode 100644 index 0b8afae..0000000 --- a/Server/Persistence/Entities/SampleModule.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Persistence.Entities; - -/// -/// Represents a module with auditable properties and a required name. -/// -public class SampleModule : AuditableModuleBase -{ - public required string Name { get; set; } -} diff --git a/Server/Persistence/Migrations/01000000_InitializeModule.cs b/Server/Persistence/Migrations/01000000_InitializeModule.cs index d839cc2..c9dd139 100644 --- a/Server/Persistence/Migrations/01000000_InitializeModule.cs +++ b/Server/Persistence/Migrations/01000000_InitializeModule.cs @@ -12,25 +12,25 @@ public InitializeModule(IDatabase database) : base(database) protected override void Up(MigrationBuilder migrationBuilder) { - var sampleModuleEntityBuilder = new SampleModuleEntityBuilder(migrationBuilder, ActiveDatabase); - sampleModuleEntityBuilder.Create(); - - var fileHubEntityBuilder = new FileHubEntityBuilder(migrationBuilder, ActiveDatabase); - fileHubEntityBuilder.Create(); + var fileEntityBuilder = new EntityBuilders.FileEntityBuilder(migrationBuilder, ActiveDatabase); + fileEntityBuilder.Create(); var categoryEntityBuilder = new CategoryEntityBuilder(migrationBuilder, ActiveDatabase); categoryEntityBuilder.Create(); + + var fileCategoryEntityBuilder = new FileCategoryEntityBuilder(migrationBuilder, ActiveDatabase); + fileCategoryEntityBuilder.Create(); } protected override void Down(MigrationBuilder migrationBuilder) { - var sampleModuleEntityBuilder = new SampleModuleEntityBuilder(migrationBuilder, ActiveDatabase); - sampleModuleEntityBuilder.Drop(); - - var fileHubEntityBuilder = new FileHubEntityBuilder(migrationBuilder, ActiveDatabase); - fileHubEntityBuilder.Drop(); + var fileEntityBuilder = new EntityBuilders.FileEntityBuilder(migrationBuilder, ActiveDatabase); + fileEntityBuilder.Drop(); var categoryEntityBuilder = new CategoryEntityBuilder(migrationBuilder, ActiveDatabase); categoryEntityBuilder.Drop(); + + var fileCategoryEntityBuilder = new FileCategoryEntityBuilder(migrationBuilder, ActiveDatabase); + fileCategoryEntityBuilder.Drop(); } } diff --git a/Server/Persistence/Migrations/EntityBuilders/FileCategoryEntityBuilder.cs b/Server/Persistence/Migrations/EntityBuilders/FileCategoryEntityBuilder.cs new file mode 100644 index 0000000..7029b3d --- /dev/null +++ b/Server/Persistence/Migrations/EntityBuilders/FileCategoryEntityBuilder.cs @@ -0,0 +1,34 @@ +// Licensed to ICTAce under the MIT license. + +namespace ICTAce.FileHub.Persistence.Migrations.EntityBuilders; + +public class FileCategoryEntityBuilder : AuditableBaseEntityBuilder +{ + private const string _entityTableName = "ICTAce_FileHub_FileCategory"; + private readonly PrimaryKey _primaryKey = new("PK_ICTAce_FileHub_FileCategory", x => x.Id); + private readonly ForeignKey _fileForeignKey = new("FK_ICTAce_FileHub_FileCategory_File", x => x.FileId, "ICTAce_FileHub_File", "Id", ReferentialAction.Cascade); + private readonly ForeignKey _categoryForeignKey = new("FK_ICTAce_FileHub_FileCategory_Category", x => x.CategoryId, "ICTAce_FileHub_Category", "Id", ReferentialAction.Restrict); + + public FileCategoryEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_fileForeignKey); + ForeignKeys.Add(_categoryForeignKey); + } + + protected override FileCategoryEntityBuilder BuildTable(ColumnsBuilder table) + { + Id = AddAutoIncrementColumn(table, "Id"); + ModuleId = AddIntegerColumn(table, "ModuleId"); + FileId = AddIntegerColumn(table, "FileId"); + CategoryId = AddIntegerColumn(table, "CategoryId"); + AddAuditableColumns(table); + return this; + } + + public OperationBuilder Id { get; set; } + public OperationBuilder ModuleId { get; set; } + public OperationBuilder FileId { get; set; } + public OperationBuilder CategoryId { get; set; } +} diff --git a/Server/Persistence/Migrations/EntityBuilders/FileHubEntityBuilder.cs b/Server/Persistence/Migrations/EntityBuilders/FileEntityBuilder.cs similarity index 59% rename from Server/Persistence/Migrations/EntityBuilders/FileHubEntityBuilder.cs rename to Server/Persistence/Migrations/EntityBuilders/FileEntityBuilder.cs index 6e55019..52fb9bc 100644 --- a/Server/Persistence/Migrations/EntityBuilders/FileHubEntityBuilder.cs +++ b/Server/Persistence/Migrations/EntityBuilders/FileEntityBuilder.cs @@ -2,25 +2,26 @@ namespace ICTAce.FileHub.Persistence.Migrations.EntityBuilders; -public class FileHubEntityBuilder : AuditableBaseEntityBuilder +public class FileEntityBuilder : AuditableBaseEntityBuilder { - private const string _entityTableName = "ICTAce_FileHub"; - private readonly PrimaryKey _primaryKey = new("PK_ICTAce_FileHub", x => x.Id); - private readonly ForeignKey _moduleForeignKey = new("FK_ICTAce_FileHub_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade); + private const string _entityTableName = "ICTAce_FileHub_File"; + private readonly PrimaryKey _primaryKey = new("PK_ICTAce_FileHub_File", x => x.Id); + private readonly ForeignKey _moduleForeignKey = new("FK_ICTAce_FileHub_File_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade); - public FileHubEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + public FileEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) { EntityTableName = _entityTableName; PrimaryKey = _primaryKey; ForeignKeys.Add(_moduleForeignKey); } - protected override FileHubEntityBuilder BuildTable(ColumnsBuilder table) + protected override FileEntityBuilder BuildTable(ColumnsBuilder table) { Id = AddAutoIncrementColumn(table, "Id"); ModuleId = AddIntegerColumn(table, "ModuleId"); - Name = AddMaxStringColumn(table, "Name"); + Name = AddStringColumn(table, "Name", 100); FileName = AddStringColumn(table, "FileName", 255); + ImageName = AddStringColumn(table, "ImageName", 255); Description = AddStringColumn(table, "Description", 1000, nullable: true); FileSize = AddStringColumn(table, "FileSize", 12); Downloads = AddIntegerColumn(table, "Downloads"); @@ -32,6 +33,7 @@ protected override FileHubEntityBuilder BuildTable(ColumnsBuilder table) public OperationBuilder ModuleId { get; set; } public OperationBuilder Name { get; set; } public OperationBuilder FileName { get; set; } + public OperationBuilder ImageName { get; set; } public OperationBuilder Description { get; set; } public OperationBuilder FileSize { get; set; } public OperationBuilder Downloads { get; set; } diff --git a/Server/Persistence/Migrations/EntityBuilders/SampleModuleEntityBuilder.cs b/Server/Persistence/Migrations/EntityBuilders/SampleModuleEntityBuilder.cs deleted file mode 100644 index 4d476ed..0000000 --- a/Server/Persistence/Migrations/EntityBuilders/SampleModuleEntityBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to ICTAce under the MIT license. - -namespace ICTAce.FileHub.Persistence.Migrations.EntityBuilders; - -public class SampleModuleEntityBuilder : AuditableBaseEntityBuilder -{ - private const string _entityTableName = "Company_SampleModule"; - private readonly PrimaryKey _primaryKey = new("PK_SampleModule", x => x.Id); - private readonly ForeignKey _moduleForeignKey = new("FK_SampleModule_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade); - - public SampleModuleEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) - { - EntityTableName = _entityTableName; - PrimaryKey = _primaryKey; - ForeignKeys.Add(_moduleForeignKey); - } - - protected override SampleModuleEntityBuilder BuildTable(ColumnsBuilder table) - { - Id = AddAutoIncrementColumn(table, "Id"); - ModuleId = AddIntegerColumn(table, "ModuleId"); - Name = AddMaxStringColumn(table, "Name"); - AddAuditableColumns(table); - return this; - } - - public OperationBuilder Id { get; set; } - public OperationBuilder ModuleId { get; set; } - public OperationBuilder Name { get; set; } -} diff --git a/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.css b/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.css deleted file mode 100644 index 0856a26..0000000 --- a/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.css +++ /dev/null @@ -1 +0,0 @@ -/* Module Custom Styles */ \ No newline at end of file diff --git a/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.js b/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.js deleted file mode 100644 index 8a2d45a..0000000 --- a/Server/wwwroot/Modules/ICTAce.FileHub.SampleModule/Module.js +++ /dev/null @@ -1,5 +0,0 @@ -/* Module Script */ -var App = App || {}; - -App.SampleModule = { -}; diff --git a/Server/wwwroot/Modules/ICTAce.FileHub/Module.css b/Server/wwwroot/Modules/ICTAce.FileHub/Module.css index 024c4bf..2832627 100644 --- a/Server/wwwroot/Modules/ICTAce.FileHub/Module.css +++ b/Server/wwwroot/Modules/ICTAce.FileHub/Module.css @@ -4,3 +4,68 @@ .rz-popup { z-index: 99999 !important; } + +/* File Card Styling */ +.rz-card { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + border: 1px solid rgba(0, 0, 0, 0.12); + margin-bottom: 1rem; + cursor: pointer; +} + +.rz-card:hover { + transform: translateY(-4px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* File thumbnail placeholder */ +.file-thumbnail-placeholder { + width: 150px; + height: 150px; + background-color: rgba(0, 0, 0, 0.03); + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + border: 2px dashed rgba(0, 0, 0, 0.12); +} + +/* File card content */ +.file-card-content h5 { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.file-card-content .text-muted { + opacity: 0.7; + font-size: 0.9rem; +} + +/* Badge spacing */ +.rz-badge { + margin-right: 0.5rem; + margin-bottom: 0.25rem; +} + +/* DataList pagination */ +.rz-datalist-pager { + margin-top: 1.5rem; +} + +/* Image styling */ +.file-card-image { + width: 150px; + height: 150px; + object-fit: cover; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .file-thumbnail-placeholder, + .file-card-image { + width: 100px !important; + height: 100px !important; + } +} diff --git a/Server/wwwroot/Modules/ICTAce.FileHub/Module.js b/Server/wwwroot/Modules/ICTAce.FileHub/Module.js index 8a2d45a..aa7c7e1 100644 --- a/Server/wwwroot/Modules/ICTAce.FileHub/Module.js +++ b/Server/wwwroot/Modules/ICTAce.FileHub/Module.js @@ -1,5 +1 @@ /* Module Script */ -var App = App || {}; - -App.SampleModule = { -};