Skip to content

Feature/issue 309 changing photo logic from base64 to url approach - #318

Merged
taraskibysh merged 21 commits into
release/1.0.0from
feature/issue-309
Sep 2, 2025
Merged

Feature/issue 309 changing photo logic from base64 to url approach#318
taraskibysh merged 21 commits into
release/1.0.0from
feature/issue-309

Conversation

@taraskibysh

@taraskibysh taraskibysh commented Aug 5, 2025

Copy link
Copy Markdown
Contributor

dev

JIRA

Code reviewers

Second Level Review

  • @github_username

Summary of issue

Research different approaches to image loading (e.g., caching, thumbnails, direct URLs) and speed up loading on the visitor team page.

Summary of change

Change file processing logic: use URLs instead of base64 data

Testing approach

unit/integration tests

CHECK LIST

  • СI passed
  • Сode coverage >=95%
  • PR is reviewed manually again (to make sure you have 100% ready code)
  • All reviewers agreed to merge the PR
  • I've checked new feature as logged in and logged out user if needed
  • PR meets all conventions

Summary by CodeRabbit

  • New Features

    • Images now return public URLs instead of embedded Base64; automatic URL mapping; team members listing now returns paginated results.
  • Bug Fixes

    • Improved validation and clearer errors for missing/invalid image names/IDs; more robust save/update/delete file handling and error reporting.
  • Chores

    • Simplified local image storage config (root + images subpath); HTTP context accessor registered and static file serving enabled.
  • Tests

    • Expanded unit/integration coverage for URL handling, MIME→extension mapping, invalid inputs, cleanup, and file operations.

@coderabbitai

coderabbitai Bot commented Aug 5, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Replaces embedded Base64 payloads with URL-based image access, refactors local blob service to filesystem + URL generation, introduces a BlobStorageExceptions hierarchy, renames Image.Base64→Url (entity/DTO), updates mappings/handlers to use URLs and TransactionScope/SaveChangesAsync, adjusts DI/config (IHttpContextAccessor, RootPath/ImagesSubPath), and updates tests/seeders and .gitignore.

Changes

Cohort / File(s) Summary
Ignore config
./.gitignore
Replace ignored pattern *StaticFile/ with *wwwroot/.
Blob exceptions (removed & added)
VictoryCenter.BLL/Exceptions/BlobStorage.cs, VictoryCenter.BLL/Exceptions/BlobStorageExceptions/*
Remove legacy single-file blob exceptions; add new VictoryCenter.BLL.Exceptions.BlobStorageExceptions types (BlobStorageException, BlobNotFoundException, BlobFileSystemException, BlobFileNameException, InvalidBase64FormatException, ImageProcessingException, BlobHttpContextException).
Blob service API & env
VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs, VictoryCenter.BLL/Services/BlobStorage/BlobService.cs, VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs
Replace async Base64 retrieval with synchronous GetFileUrl(name,mimeType); add memory-stream/update/delete helpers; require IHttpContextAccessor; change env to RootPath + ImagesSubPath with computed FullPath; implement filesystem read/write/delete and URL construction; remove encryption helpers.
Image DTO/entity/mapping
VictoryCenter.BLL/DTOs/Images/ImageDTO.cs, VictoryCenter.DAL/Entities/Image.cs, VictoryCenter.DAL/Data/.../ImageConfig.cs, VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs, VictoryCenter.BLL/Mapping/Images/ImageToUrlResolver.cs
Rename Base64Url on DTO and entity (Url init-only on DTO); EF config ignores Url; add BlobToUrlResolver and update AutoMapper to populate Url via IBlobService.GetFileUrl.
Image handlers & queries
VictoryCenter.BLL/Commands/Images/*, VictoryCenter.BLL/Queries/Images/*, VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs
Switch transaction usage to TransactionScope, set CreatedAt on create, persist via repository + SaveChangesAsync, remove re-fetching Base64 from storage, map created/updated entities to ImageDTO.Url, add BlobStorageException catches and nullability guards.
Team member handlers & queries
VictoryCenter.BLL/Commands/TeamMembers/*, VictoryCenter.BLL/Queries/TeamMembers/*
Remove IBlobService injection where unused, stop embedding Base64 into DTOs, wrap create/update flows in TransactionScope, adjust GetTeamMembersByFilters to return PaginationResult<TeamMemberDto> and compute total count.
Programs & categories handlers
VictoryCenter.BLL/Commands/Programs/*, VictoryCenter.BLL/Queries/Programs/*, VictoryCenter.BLL/Queries/ProgramCategories/*
Remove inline Base64 enrichment and drop unused IBlobService injections; map responses without embedding Base64.
Constants
VictoryCenter.BLL/Constants/ImageConstants.cs
Remove legacy encryption/IV constants; add FailedToSaveImage, FailedToReadImage, HttpContextIsNotAvailable, FailToCreateDirectory and helper WrongFileName(name).
Web startup & config
VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs, VictoryCenter.WebAPI/Program.cs, appsettings*.json
Post-configure BlobEnvironmentVariables.RootPath from web root, register AddHttpContextAccessor() and UseStaticFiles(), replace BlobStoreKey/BlobStorePath with ImagesSubPath in appsettings.
Tests — unit & integration
VictoryCenter.UnitTests/**, VictoryCenter.IntegrationTests/ControllerTests/Images/*, VictoryCenter.UnitTests/ServiceTests/BlobService.cs
Update tests to expect Url and use FullPath; add MIME-extension parameterized tests and invalid-input tests; remove/adapt IBlobService mocks; add BlobService tests for URL building and error paths; fixture cleans up image directory.
Seeders & fixtures
IntegrationTests/Utils/Seeders/Images/*, IntegrationTests/ControllerTests/DbFixture/IntegrationTestDbFixture.cs
Add ImageEntityForSeed and use it for seeding; stop assigning Base64 into persisted Image entity; seeder still writes files via BlobService; fixture dispose deletes BlobEnvironmentVariables.FullPath.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as WebAPI
  participant Handler as CreateImageHandler
  participant Repo as Repository
  participant Blob as BlobService
  note right of Handler #f7f7d9: create flow (DB first, then file)
  Client->>API: POST /api/images (Base64, MimeType)
  API->>Handler: Handle(command)
  Handler->>Repo: Begin TransactionScope
  Handler->>Repo: CreateAsync(image entity)
  Handler->>Repo: SaveChangesAsync()
  Handler->>Blob: SaveFileInStorageAsync(Base64, BlobName, MimeType)
  Handler->>Repo: scope.Complete()
  Handler->>Blob: GetFileUrl(BlobName, MimeType)
  Handler->>API: Result<ImageDTO { Url, ... }>
  API-->>Client: 200 OK + DTO
Loading
sequenceDiagram
  autonumber
  participant Client
  participant API as WebAPI
  participant Handler as GetImageByIdHandler
  participant Repo as Repository
  participant Blob as BlobService
  Client->>API: GET /api/images/{id}
  API->>Handler: Handle(query)
  Handler->>Repo: Find image by id
  alt image exists & BlobName valid
    Handler->>Blob: GetFileUrl(BlobName, MimeType)
    Handler->>API: Result<ImageDTO { Url }>
    API-->>Client: 200 OK
  else missing / invalid
    Handler-->>API: Error Result (400/404)
    API-->>Client: 400/404
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

back-end

Suggested reviewers

  • maxvonlancaster
  • MarkBevz50
  • VladimirSushinsky

"A blob once stored as base64 tucked away,
Now carries a URL that greets the day.
Transactions tidy, exceptions redefined,
Tests seed the ground and fixtures clear behind.
Files on disk hum — the images step outside."

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-309

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🔭 Outside diff range comments (2)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)

80-87: Remove unused image retrieval logic.

This code fetches an image from the repository but doesn't use the result for anything. Since the blob service dependency was removed, this appears to be leftover code from the refactoring.

var resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);
-if (entityToUpdate.ImageId != null)
-{
-    Image? image = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
-        new QueryOptions<Image>()
-        {
-            Filter = i => i.Id == entityToUpdate.ImageId
-        });
-}

95-98: Remove unreachable exception handler.

The BlobStorageException catch block is now unreachable since all blob storage operations were removed from this handler. This creates dead code that could confuse future maintainers.

-catch (BlobStorageException e)
-{
-    return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}" );
-}
🧹 Nitpick comments (8)
VictoryCenter/VictoryCenter.DAL/Entities/Image.cs (1)

12-12: Consider using System.Uri type for stronger type safety.

The static analysis tool correctly identifies that URL properties should ideally use System.Uri instead of string for better type safety and validation. However, this depends on your architecture decisions:

Pros of using Uri:

  • Built-in URL validation
  • Type safety prevents invalid URL assignments
  • Clear semantic intent

Potential considerations:

  • EF Core serialization behavior with Uri types
  • JSON serialization in API responses
  • Consistency with existing codebase patterns

If you decide to keep string, consider adding validation attributes or custom validation logic to ensure URL validity.

-    public string? Url { get; set; }
+    public Uri? Url { get; set; }
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (1)

6-6: Consider parameter naming consistency and return type.

The new GetFileUrl method has a few minor considerations:

  1. Parameter naming inconsistency: mimeType (camelCase) vs mimetype (lowercase). For consistency with line 7, consider using mimeType:
-    string GetFileUrl(string name, string mimeType);
+    string GetFileUrl(string name, string mimeType);
  1. Synchronous method in async interface: While this method being synchronous makes sense for URL generation, it creates a mixed pattern. Consider documenting why this method doesn't need to be async.

  2. Return type consideration: Similar to other files, consider Uri instead of string for type safety, though this should be consistent across the codebase.

The method design aligns well with the architectural changes - URL generation is typically a lightweight, synchronous operation.

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (1)

80-83: Consider removing obsolete exception handling.

Since the handler no longer directly uses IBlobService, the BlobStorageException catch block may be unnecessary. The AutoMapper resolver might handle blob storage exceptions differently.

Verify if BlobStorageException can still be thrown in this context:

#!/bin/bash
# Description: Check if BlobStorageException is still thrown by AutoMapper or other dependencies

# Search for BlobStorageException usage in AutoMapper resolvers and related code
ast-grep --pattern 'throw new BlobStorageException($$$)'
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (2)

101-131: Consider adding proper cleanup mechanism for test data

The cleanup operation at line 130 might not execute if any assertion fails, potentially leaving test data in the system. Consider wrapping the test logic in a try-finally block or using a more robust cleanup pattern.

 [Fact]
 public async Task DeleteImage_MultipleImages_ShouldDeleteOnlySpecified()
 {
     // Arrange: Створюємо два тестових зображення
     var image1 = await CreateTestImageAsync("test-image-1");
     var image2 = await CreateTestImageAsync("test-image-2");
 
-    string filePath1 = Path.Combine(_blobEnvironment.FullPath, $"{image1.BlobName}.png");
-    string filePath2 = Path.Combine(_blobEnvironment.FullPath, $"{image2.BlobName}.png");
-
-    // Act: Видаляємо тільки перше зображення
-    HttpResponseMessage response = await _client.DeleteAsync($"api/Image/{image1.Id}");
-
-    // Assert: Перевіряємо результат
-    Assert.True(response.IsSuccessStatusCode);
-
-    // Перше зображення має бути видалене
-    var deletedImage1 = await _dbContext.Images.AsNoTracking()
-        .FirstOrDefaultAsync(e => e.Id == image1.Id);
-    Assert.Null(deletedImage1);
-    Assert.False(File.Exists(filePath1));
-
-    // Друге зображення має залишитися
-    var remainingImage2 = await _dbContext.Images.AsNoTracking()
-        .FirstOrDefaultAsync(e => e.Id == image2.Id);
-    Assert.NotNull(remainingImage2);
-    Assert.True(File.Exists(filePath2));
-
-    // Cleanup: Видаляємо друге зображення
-    await _client.DeleteAsync($"api/Image/{image2.Id}");
+    try
+    {
+        string filePath1 = Path.Combine(_blobEnvironment.FullPath, $"{image1.BlobName}.png");
+        string filePath2 = Path.Combine(_blobEnvironment.FullPath, $"{image2.BlobName}.png");
+
+        // Act: Видаляємо тільки перше зображення
+        HttpResponseMessage response = await _client.DeleteAsync($"api/Image/{image1.Id}");
+
+        // Assert: Перевіряємо результат
+        Assert.True(response.IsSuccessStatusCode);
+
+        // Перше зображення має бути видалене
+        var deletedImage1 = await _dbContext.Images.AsNoTracking()
+            .FirstOrDefaultAsync(e => e.Id == image1.Id);
+        Assert.Null(deletedImage1);
+        Assert.False(File.Exists(filePath1));
+
+        // Друге зображення має залишитися
+        var remainingImage2 = await _dbContext.Images.AsNoTracking()
+            .FirstOrDefaultAsync(e => e.Id == image2.Id);
+        Assert.NotNull(remainingImage2);
+        Assert.True(File.Exists(filePath2));
+    }
+    finally
+    {
+        // Cleanup: Видаляємо друге зображення
+        await _client.DeleteAsync($"api/Image/{image2.Id}");
+    }
 }

133-136: Use long literals in InlineData attributes

Based on the VictoryCenter project conventions, InlineData attributes should provide long literals when testing long parameters.

 [Theory]
-[InlineData(-1)]
-[InlineData(0)]
+[InlineData(-1L)]
+[InlineData(0L)]
 public async Task DeleteImage_InvalidIdValues_ShouldReturnNotFound(long invalidId)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)

68-71: Use long literals in InlineData attributes

For consistency with VictoryCenter project conventions, use long literals in InlineData attributes.

 [Theory]
-[InlineData(-1)]
-[InlineData(0)]
+[InlineData(-1L)]
+[InlineData(0L)]
 public async Task GetImageById_InvalidIdValues_ShouldReturnNotFound(long invalidId)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1)

230-249: Simplify code by removing redundant variable

The subString variable at line 234 is unnecessary as it just copies the subPath parameter without any transformation.

 private async Task<Image> CreateTestImageAsync(string? subPath, string blobName, string? customMimeType = null )
 {
     var mimeType = customMimeType ?? "image/png";
     var extension = GetExtensionFromMimeType(mimeType);
-    var subString = subPath;
-    var url = $"http://localhost/{subString}/{blobName}.{extension}";
+    var url = $"http://localhost/{subPath}/{blobName}.{extension}";
 
     var image = new Image
     {
         BlobName = blobName,
         MimeType = mimeType,
         Url = url,
         CreatedAt = DateTime.UtcNow
     };
 
     _dbContext.Images.Add(image);
     await _dbContext.SaveChangesAsync();
 
     return image;
 }
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs (1)

102-105: Use long literals in InlineData attributes

For consistency with VictoryCenter project conventions, use long literals in InlineData attributes.

 [Theory]
-[InlineData(-1)]
-[InlineData(0)]
+[InlineData(-1L)]
+[InlineData(0L)]
 public async Task UpdateImage_InvalidIdValues_ShouldReturnNotFound(long invalidId)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ac4ed3 and cade56c.

📒 Files selected for processing (37)
  • .gitignore (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetPublished/GetPublishedTeamMembersHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5 hunks)
  • VictoryCenter/VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs (1 hunks)
  • VictoryCenter/VictoryCenter.DAL/Entities/Image.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (3 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (3 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs (4 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (2 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (3 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs (5 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/UpdateImage.cs (2 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs (4 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (5 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (2 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs (2 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/Program.cs (2 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/appsettings.json (1 hunks)
💤 Files with no reviewable changes (6)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetPublished/GetPublishedTeamMembersHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: in the victorycenter project, the jwt secret key in appsettings.json is temporarily hard-coded for d...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.WebAPI/appsettings.json:16-16
Timestamp: 2025-06-26T08:26:15.124Z
Learning: In the VictoryCenter project, the JWT secret key in appsettings.json is temporarily hard-coded for development purposes and will be removed/replaced with secure configuration in future stages.

Applied to files:

  • VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json
  • VictoryCenter/VictoryCenter.WebAPI/appsettings.json
  • VictoryCenter/VictoryCenter.WebAPI/Program.cs
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
📚 Learning: in the victorycenter project integration tests, using an empty claims array for createaccesstoken in...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Base/IntegrationTestDbFixture.cs:48-48
Timestamp: 2025-06-27T08:50:52.032Z
Learning: In the VictoryCenter project integration tests, using an empty claims array for CreateAccessToken in the IntegrationTestDbFixture is sufficient for authentication purposes, as confirmed by the project maintainer.

Applied to files:

  • VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
📚 Learning: in the victorycenter test seeder for teammember entities, the last category in the categories list i...
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#114
File: VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/TeamMembersSeeder/TeamMemberSeeder.cs:20-24
Timestamp: 2025-06-17T20:32:16.009Z
Learning: In the VictoryCenter test seeder for TeamMember entities, the last category in the categories list is intentionally excluded from having team members assigned to it (using `categories[i % (categories.Count - 1)].Id`). This design ensures that the last category remains available for delete tests without foreign key constraint violations, as delete operations require categories with no related team members.

Applied to files:

  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs
  • VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs
📚 Learning: in victorycenter project, when writing unit tests for deleteteammembercommand, the parameter types s...
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#177
File: VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/DeleteTeamMemberTests.cs:47-47
Timestamp: 2025-06-20T18:50:30.605Z
Learning: In VictoryCenter project, when writing unit tests for DeleteTeamMemberCommand, the parameter types should use `long` to match the command constructor and entity ID type. InlineData attributes should provide `long` literals (e.g., -1L, 0L) rather than int values.

Applied to files:

  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
📚 Learning: in the victorycenter project, team member categories typically contain about 10 members in productio...
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:48-54
Timestamp: 2025-06-20T18:26:26.262Z
Learning: In the VictoryCenter project, team member categories typically contain about 10 members in production, so O(n²) operations on team members are not performance-critical and readability should be prioritized over micro-optimizations.

Applied to files:

  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs
📚 Learning: in the victorycenter codebase, fluentvalidation is used for input validation in mediatr handlers. th...
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.

Applied to files:

  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs
🪛 GitHub Check: Build and analyze
VictoryCenter/VictoryCenter.DAL/Entities/Image.cs

[warning] 12-12:
Change the 'Url' property type to 'System.Uri'. (https://rules.sonarsource.com/csharp/RSPEC-3996)


[warning] 12-12:
Change the 'Url' property type to 'System.Uri'. (https://rules.sonarsource.com/csharp/RSPEC-3996)

🔇 Additional comments (28)
.gitignore (1)

161-161: Confirm wwwroot exclusion won’t drop essential static assets

The pattern *wwwroot/ will cause git to ignore every folder named wwwroot at any depth.
If your web projects still rely on checked-in fallback images, CSS, or JS that must ship with the artifact, those files will silently become untracked. Please verify that all necessary static content is produced during build/deploy (or fetched from blob storage) before keeping this rule; otherwise narrow the pattern or add explicit negations for required files.

VictoryCenter/VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs (1)

29-29: LGTM! Configuration correctly updated to match entity changes.

The update to ignore the Url property instead of Base64 is consistent with the entity model changes and maintains the correct behavior of not persisting dynamically generated URLs to the database.

VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs (1)

40-55: Good refactoring to align with new architecture

The renaming from BlobCryptographyException to ImageProcessingException properly reflects the removal of encryption logic and the shift to general image processing. The exception structure maintains proper inheritance and constructor patterns.

VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1)

8-8: Verify the ImagesSubPath value for potential typo

The value "IntegrationTekdnkdsts" appears to contain a typo. Was this intended to be "IntegrationTests"?

If this is intentional for uniqueness in testing, please confirm. Otherwise, consider correcting:

-      "ImagesSubPath": "IntegrationTekdnkdsts"
+      "ImagesSubPath": "IntegrationTests"
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs (1)

23-23: Correct update to use new path property

The change from BlobStorePath to FullPath properly aligns with the refactored BlobEnvironmentVariables structure.

VictoryCenter/VictoryCenter.WebAPI/appsettings.json (1)

8-8: Configuration simplification looks good!

The replacement of separate BlobStoreKey and BlobStorePath with a single ImagesSubPath property aligns well with the transition to URL-based image handling. This simplification makes the configuration more maintainable.

VictoryCenter/VictoryCenter.WebAPI/Program.cs (2)

13-13: Essential addition for URL generation!

Adding HttpContextAccessor is crucial for the blob service to generate full URLs based on the current HTTP request context. This supports the transition from Base64 to URL-based image handling.


32-32: Static file serving correctly configured!

The UseStaticFiles() middleware enables direct serving of image files from the file system, which is essential for the new URL-based approach. The placement in the middleware pipeline is appropriate.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/UpdateImage.cs (2)

42-42: Test data correctly updated for URL-based approach!

The change from Base64 to Url property in the test DTO aligns perfectly with the broader refactoring to URL-based image handling.


97-97: Assertion properly updated!

The test assertion now correctly validates the Url property instead of Base64, maintaining test coverage for the new image handling approach.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (2)

41-41: Test DTO correctly aligned with new architecture!

The update from Base64 to Url property in the test data properly reflects the transition to URL-based image handling.


93-93: Assertion updated appropriately!

The test now validates the Url property instead of Base64, ensuring continued test coverage for the refactored image handling logic.

VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (2)

52-52: Good preservation of original MIME type!

Capturing the previous MIME type before updating the entity is a smart approach that allows the blob service to properly handle the file type transition during updates.


64-64: Confirmed blob service signature matches usage
The IBlobService.UpdateFileInStorageAsync method in IBlobService.cs indeed accepts the original MIME type (previousMimeType) followed by the new blob name and new MIME type, and the BlobService implementation in BlobService.cs aligns with this signature. The call in UpdateImageHandler.cs (line 64) is correct—no changes required.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (1)

66-66: LGTM! Assertion updated correctly.

The test assertion properly validates the URL property instead of Base64, aligning with the refactoring objectives.

VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs (2)

221-235: LGTM! Ukrainian text properly encoded.

The category data has been corrected with proper Ukrainian text encoding, improving readability and data quality.


293-299: LGTM! Proper configuration for local blob storage.

The PostConfigure step correctly sets the RootPath from the web hosting environment, supporting the new blob storage structure with RootPath and ImagesSubPath properties.

VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)

5-8: LGTM! Well-designed configuration structure.

The new properties properly support the blob storage refactoring:

  • Required properties ensure configuration completeness
  • Setters enable runtime configuration via PostConfigure
  • Path.Combine ensures cross-platform compatibility
  • FullPath provides convenient access to the combined path
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/CreateTeamMemberTests.cs (1)

75-75: LGTM! Test constructor calls updated consistently.

All handler instantiations have been properly updated to remove the IBlobService dependency, aligning with the refactored CreateTeamMemberHandler constructor.

Also applies to: 91-91, 110-110, 140-140

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (1)

22-22: LGTM! Constructor updated correctly.

The IBlobService dependency has been properly removed, aligning with the shift to URL-based image handling through AutoMapper configuration.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs (2)

29-32: LGTM! Test data properly updated for URL-based approach.

The test setup correctly reflects the new architecture with BlobName excluding the file extension and the Url property containing the expected URL.


50-51: Mock setup correctly updated for synchronous URL generation.

The change from FindFileInStorageAsBase64Async to GetFileUrl aligns with the new URL-based approach and properly returns a URL string instead of Base64 content.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (1)

124-124: Clean removal of blob service dependency.

The handler constructor calls have been correctly updated to remove the IBlobService parameter, maintaining consistency with the refactored UpdateTeamMemberHandler that no longer requires blob storage functionality.

Also applies to: 157-157, 183-183, 204-204, 223-223

VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/ImageSeeder/ImagesSeeder.cs (2)

9-21: Well-structured data format for seeding.

The tuple-based data structure clearly separates blob metadata (blobName, base64Data, mimeType) and provides a clean foundation for the new seeding process.


27-45: Proper implementation of URL-based image seeding.

The seeding logic correctly:

  • Saves files to blob storage using the service
  • Generates URLs through the blob service
  • Creates Image entities with the generated URL
  • Uses proper async patterns

This aligns perfectly with the architectural shift from Base64 to URL-based image handling.

VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)

23-31: Proper test setup for URL-based blob service.

The configuration correctly uses the new BlobEnvironmentVariables structure with RootPath and ImagesSubPath, and includes the required IHttpContextAccessor mock for URL generation.


56-71: Excellent test refactoring for URL generation.

The test properly mocks the HTTP context and request to simulate URL generation, and verifies the expected HTTPS URL format including the subdirectory path. This thoroughly validates the new URL-based functionality.


43-43: Correct assertion for content verification.

Changing from Assert.NotEqual to Assert.Equal makes sense here - we want to verify that the stored file content matches the original content, not that it's different (which was likely the case when encryption was involved).

public long Id { get; init; }
public string BlobName { get; init; } = null!;
public string Base64 { get; set; } = null!;
public string Url { get; set; } = null!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Address property mutability inconsistency.

The Url property uses { get; set; } while other properties in this record use { get; init; }. This breaks the immutable pattern typically expected with record types.

If AutoMapper requires a setter for mapping, consider using { get; init; } and ensure the mapping configuration properly handles initialization. If mutability is truly required, document why this property needs to be mutable when others don't.

-    public string Url { get; set; } = null!;
+    public string Url { get; init; } = null!;

Additionally, consider the same type safety improvement mentioned for the entity:

-    public string Url { get; init; } = null!;
+    public Uri Url { get; init; } = null!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public string Url { get; set; } = null!;
public Uri Url { get; init; } = null!;
🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs at line 6, the Url
property is declared with a mutable setter { get; set; } while other properties
use immutable setters { get; init; }, breaking the record's immutability
pattern. Change the Url property to use { get; init; } to maintain consistency
and immutability. If AutoMapper requires a setter, adjust the mapping
configuration to support init-only properties. Also, review and apply any
suggested type safety improvements for this property as mentioned for the
entity.

Comment thread VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs Outdated
Comment thread VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs Outdated
Comment on lines 311 to 321
private static string GetExtensionFromMimeType(string mimeType)
{
return mimeType.ToLower() switch
{
"image/jpeg" => "jpg",
"image/jpg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
_ => "jpg"
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract duplicated helper method to shared test utilities

The GetExtensionFromMimeType method is duplicated across all five test files. This violates the DRY principle and makes maintenance harder.

Create a shared test helper class:

// In a new file: VictoryCenter.IntegrationTests/ControllerTests/Images/Helpers/ImageTestHelpers.cs
namespace VictoryCenter.IntegrationTests.ControllerTests.Images.Helpers;

public static class ImageTestHelpers
{
    public static string GetExtensionFromMimeType(string mimeType)
    {
        return mimeType.ToLower() switch
        {
            "image/jpeg" => "jpg",
            "image/jpg" => "jpg",
            "image/png" => "png",
            "image/webp" => "webp",
            _ => "jpg"
        };
    }
}

Then update all test files to use the shared helper:

-private static string GetExtensionFromMimeType(string mimeType)
-{
-    return mimeType.ToLower() switch
-    {
-        "image/jpeg" => "jpg",
-        "image/jpg" => "jpg",
-        "image/png" => "png",
-        "image/webp" => "webp",
-        _ => "jpg"
-    };
-}
+// Use ImageTestHelpers.GetExtensionFromMimeType(mimeType) instead
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
around lines 311 to 321, the GetExtensionFromMimeType method is duplicated
across multiple test files. To fix this, create a new static helper class
ImageTestHelpers in a new file
VictoryCenter.IntegrationTests/ControllerTests/Images/Helpers/ImageTestHelpers.cs
containing this method. Then remove the duplicate method from this file and all
other test files, and update their code to call
ImageTestHelpers.GetExtensionFromMimeType instead.

Comment thread VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs Outdated
Comment thread VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1)

229-248: Helper method doesn't create physical files - inconsistent with other test classes

Unlike the CreateTestImageAsync method in GetImageByIdTest.cs, this version only creates database records without corresponding physical files. This inconsistency could lead to false positive test results if the API expects files to exist in blob storage.

Add physical file creation to match the implementation in other test classes:

 _dbContext.Images.Add(image);
 await _dbContext.SaveChangesAsync();

+// Create physical test file for realistic testing
+var filePath = Path.Combine(_blobEnv.FullPath, $"{blobName}.{extension}");
+Directory.CreateDirectory(_blobEnv.FullPath);
+var testImageBytes = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=");
+await File.WriteAllBytesAsync(filePath, testImageBytes);

 return image;
🧹 Nitpick comments (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)

44-64: Consider using English for code comments for better international collaboration

While the test implementation is solid, the Ukrainian comments (e.g., "Перевіряємо що відповідь успішна") might create barriers for international contributors. Consider using English to maintain consistency and improve maintainability across diverse teams.

VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)

54-66: Use a more robust approach for non-existent ID testing

Using long.MaxValue could theoretically collide with an actual ID if the database sequence reaches this value. Consider using a negative value or querying for the max ID and adding an offset to ensure the ID truly doesn't exist.

-    var nonExistentId = long.MaxValue;
+    // Use negative ID which is guaranteed to not exist
+    var nonExistentId = -999L;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cade56c and 36eebcc.

📒 Files selected for processing (11)
  • VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (3 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (3 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (3 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs
🚧 Files skipped from review as they are similar to previous changes (7)
  • VictoryCenter/VictoryCenter.BLL/DTOs/Images/ImageDTO.cs
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImagesProfile.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageByName.cs
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageResolver/ImageToUrlResolver.cs
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: in the victorycenter project, the jwt secret key in appsettings.json is temporarily hard-coded for d...
Learnt from: NovickVitaliy
PR: ita-social-projects/VictoryCenter-Back#201
File: VictoryCenter/VictoryCenter.WebAPI/appsettings.json:16-16
Timestamp: 2025-06-26T08:26:15.124Z
Learning: In the VictoryCenter project, the JWT secret key in appsettings.json is temporarily hard-coded for development purposes and will be removed/replaced with secure configuration in future stages.

Applied to files:

  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze

Comment on lines +168 to 178
private static string GetExtensionFromMimeType(string mimeType)
{
return mimeType.ToLower() switch
{
"image/jpeg" => "jpg",
"image/jpg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
_ => "jpg"
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract duplicate helper method to a shared test utility class

The GetExtensionFromMimeType method is duplicated across multiple test files (CreateImageTests.cs, GetImageByIdTest.cs, GetImageByNameTest.cs). Consider extracting this to a shared test utility class to follow the DRY principle and ensure consistent MIME type handling across all tests.

Create a shared utility class:

public static class ImageTestHelper
{
    public static string GetExtensionFromMimeType(string mimeType)
    {
        return mimeType.ToLower() switch
        {
            "image/jpeg" => "jpg",
            "image/jpg" => "jpg", 
            "image/png" => "png",
            "image/webp" => "webp",
            _ => "jpg"
        };
    }
}
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs
around lines 168 to 178, the GetExtensionFromMimeType method is duplicated in
multiple test files. Extract this method into a new shared static utility class,
for example ImageTestHelper, within the test project. Replace all occurrences of
this method in the test files with calls to
ImageTestHelper.GetExtensionFromMimeType to centralize the logic and avoid
duplication.

Comment on lines +83 to +103
[Fact]
public async Task GetImageByName_ImageWithEmptyBlobName_ShouldReturnError()
{
// Arrange: Створюємо зображення з порожнім BlobName
var imageWithEmptyBlobName = new Image
{
BlobName = "", // Порожнє значення
MimeType = "image/png",
Url = "http://test.com/empty.png",
CreatedAt = DateTime.UtcNow
};

_dbContext.Images.Add(imageWithEmptyBlobName);
await _dbContext.SaveChangesAsync();

// Act: Намагаємося отримати зображення за порожнім ім'ям
HttpResponseMessage response = await _client.GetAsync($"api/Image/by-name/");

// Assert: Перевіряємо що повертається помилка (скоріше за все BadRequest через маршрутизацію)
Assert.False(response.IsSuccessStatusCode);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Test logic issue similar to GetImageByIdTest

This test has the same conceptual issue - it creates an image with an empty BlobName in the database and then expects retrieval to fail. Additionally, the test makes a request to /api/Image/by-name/ without a name parameter, which tests route handling rather than empty BlobName validation.

Consider restructuring this test to either:

  1. Verify that empty BlobName validation happens at creation time
  2. Test proper 404 response for genuinely missing names
  3. If testing route parameter validation, name the test accordingly
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
between lines 83 and 103, the test incorrectly expects retrieval failure for an
image with an empty BlobName by calling the API without a name parameter, which
tests routing rather than BlobName validation. To fix this, restructure the test
to either validate that empty BlobName is rejected during image creation, or
test that requesting a non-existent valid name returns a 404, or rename the test
to reflect it is checking route parameter validation. Adjust the test logic and
assertions accordingly to match the chosen approach.

Comment on lines +105 to +125
[Fact]
public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch()
{
// Arrange: Створюємо зображення з конкретним регістром
var originalBlobName = "TestImageName";
var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName);

// Act & Assert: Тестуємо точне співпадіння
HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}");
Assert.True(exactResponse.IsSuccessStatusCode);

// Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive)
var lowerCaseName = originalBlobName.ToLower();
if (lowerCaseName != originalBlobName)
{
HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}");

// Результат залежить від налаштувань БД (case-sensitive чи ні)
// Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incomplete test - missing assertion for case sensitivity behavior

The test performs a case-insensitive query but doesn't assert the result. This leaves the test incomplete and doesn't document the expected behavior. Either assert the expected outcome based on your database configuration or make this a parameterized test that handles both scenarios.

-            // Результат залежить від налаштувань БД (case-sensitive чи ні)
-            // Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive
+            // Assert based on expected database behavior
+            // For SQL Server (case-insensitive):
+            Assert.True(caseResponse.IsSuccessStatusCode);
+            // For PostgreSQL (case-sensitive):
+            // Assert.Equal(HttpStatusCode.NotFound, caseResponse.StatusCode);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[Fact]
public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch()
{
// Arrange: Створюємо зображення з конкретним регістром
var originalBlobName = "TestImageName";
var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName);
// Act & Assert: Тестуємо точне співпадіння
HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}");
Assert.True(exactResponse.IsSuccessStatusCode);
// Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive)
var lowerCaseName = originalBlobName.ToLower();
if (lowerCaseName != originalBlobName)
{
HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}");
// Результат залежить від налаштувань БД (case-sensitive чи ні)
// Зазвичай SQL Server case-insensitive, PostgreSQL case-sensitive
}
}
[Fact]
public async Task GetImageByName_CaseSensitivityTest_ShouldFindExactMatch()
{
// Arrange: Створюємо зображення з конкретним регістром
var originalBlobName = "TestImageName";
var testImage = await CreateTestImageAsync(_blobEnv.ImagesSubPath, originalBlobName);
// Act & Assert: Тестуємо точне співпадіння
HttpResponseMessage exactResponse = await _client.GetAsync($"api/Image/by-name/{originalBlobName}");
Assert.True(exactResponse.IsSuccessStatusCode);
// Act & Assert: Тестуємо різний регістр (має не знайти, якщо пошук case-sensitive)
var lowerCaseName = originalBlobName.ToLower();
if (lowerCaseName != originalBlobName)
{
HttpResponseMessage caseResponse = await _client.GetAsync($"api/Image/by-name/{lowerCaseName}");
// Assert based on expected database behavior
// For SQL Server (case-insensitive):
Assert.True(caseResponse.IsSuccessStatusCode);
// For PostgreSQL (case-sensitive):
// Assert.Equal(HttpStatusCode.NotFound, caseResponse.StatusCode);
}
}
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
around lines 105 to 125, the test performs a case-insensitive query but lacks an
assertion to verify the expected behavior. To fix this, add an assertion after
the case-insensitive GET request to explicitly check whether the response is
successful or not, depending on your database's case sensitivity settings.
Alternatively, refactor the test into a parameterized test that can handle both
case-sensitive and case-insensitive scenarios with appropriate assertions.

@taraskibysh
taraskibysh marked this pull request as ready for review August 6, 2025 16:37

@maxvonlancaster maxvonlancaster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, LGTM!
There are some minor stuff, but I do not want to stop development with it, and we can discuss it on our synch ups

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (3)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (1)

250-250: Potential data inconsistency in test fixture.

There's a mismatch in the test data: TeamMemberDto with Id = 2 has CategoryId = 12, but the corresponding TeamMember entity has CategoryId = 2. This inconsistency could lead to unreliable test results.

-                CategoryId = 12
+                CategoryId = 2
VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (1)

59-63: Critical: Blob name change isn’t persisted; SaveChanges is called too early and incorrectly treated as a failure on 0

  • The early SaveChangesAsync means the later assignment imageEntity.BlobName = updatedBlobName is never saved to DB.
  • SaveChangesAsync returns 0 when nothing changed; that’s not a failure. Using <= 0 can incorrectly abort updates where only the blob content changes (same MimeType).

Move SaveChangesAsync after you set BlobName and drop the <= 0 check.

Apply this diff:

-            if (await _repositoryWrapper.SaveChangesAsync() <= 0)
-            {
-                return Result.Fail<ImageDTO>(ImageConstants.FailToUpdateImage);
-            }
+            // Defer persisting DB changes until both MimeType and BlobName are finalized.
             var updatedBlobName = await _blobService.UpdateFileInStorageAsync(
                 imageEntity.BlobName,
-                previousType,
+                previousType,
                 request.UpdateImageDto.Base64!,
                 imageEntity.BlobName,
                 request.UpdateImageDto.MimeType!);
 
             imageEntity.BlobName = updatedBlobName;
 
+            // Persist MimeType and BlobName together in a single transaction.
+            await _repositoryWrapper.SaveChangesAsync();
 
             ImageDTO resultDto = _mapper.Map<Image, ImageDTO>(imageEntity);
 
             transaction.Complete();

Also applies to: 71-77

VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)

63-67: Remove debug variable from production code.

Line 65 contains a test variable assignment that appears to be left over from debugging. This should be removed.

 catch (BlobStorageException e)
 {
-    var test = ErrorMessagesConstants.BlobStorageError(e.Message);
     return Result.Fail<ImageDTO>(ErrorMessagesConstants.BlobStorageError(e.Message));
 }
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)

15-20: Harden directory creation in the constructor (wrap with try/catch).

Creating directories can fail (permissions, IO issues). Wrap it and surface a BlobFileSystemException with context. This was previously suggested and remains applicable.

Apply this diff:

     public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor)
     {
         _blobEnv = environment.Value;
         _httpContextAccessor = httpContextAccessor;
-        Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath));
+        try
+        {
+            Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath));
+        }
+        catch (Exception ex)
+        {
+            throw new BlobFileSystemException(
+                Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath),
+                "Failed to create blob storage directory",
+                ex);
+        }
     }
🧹 Nitpick comments (34)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (5)

17-17: Remove unused IBlobService dependency from the handler

The handler doesn’t use _blobService; AutoMapper resolvers can obtain IBlobService via DI independently. Dropping the field and ctor parameter reduces noise and avoids “assigned but never used” warnings.

Apply:

-    private readonly IBlobService _blobService;
     private readonly IMapper _mapper;
     private readonly IRepositoryWrapper _repository;

-    public GetTeamMemberByIdHandler(IMapper mapper, IRepositoryWrapper repository, IBlobService blobService)
+    public GetTeamMemberByIdHandler(IMapper mapper, IRepositoryWrapper repository)
     {
         _mapper = mapper;
         _repository = repository;
-        _blobService = blobService;
     }

Also applies to: 21-26


32-36: Consider NoTracking for this read-only query

If your QueryOptions supports it, enable AsNoTracking/DisableTracking to avoid change-tracker overhead for a simple read.


45-47: Make the mapped DTO non-nullable

Mapping from a non-null entity should produce a non-null DTO. Avoid nullable result to reduce downstream null checks.

-            TeamMemberDto? result = _mapper.Map<TeamMemberDto>(teamMember);
+            TeamMemberDto result = _mapper.Map<TeamMemberDto>(teamMember);

49-52: Sanitize blob storage error messages

BlobStorageError currently takes and echoes the raw exception message (e.Message), which can leak internal/storage details. Instead, return a generic message to callers and log the full exception server-side:

• Add a new overload or constant in ErrorMessagesConstants, e.g.

public static string BlobStorageError() => "An error occurred accessing blob storage.";

• Inject ILogger<GetTeamMemberByIdHandler> into the handler and log the exception:

logger.LogError(e, "Failed to fetch team member blob for Id {TeamMemberId}", request.Id);

• Update the catch to:

catch (BlobStorageException e)
{
    logger.LogError(e, "…");
    return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.BlobStorageError());
}

• Apply the same pattern to other handlers catching BlobStorageException (e.g., image handlers).


28-39: Add a cancellation check in the handler
I’ve confirmed that IRepositoryBase.GetFirstOrDefaultAsync(QueryOptions<T>?) has no overload accepting a CancellationToken. To avoid doing the database work when the operation is cancelled, short-circuit at the top of your handler:

 public async Task<Result<TeamMemberDto>> Handle(GetTeamMemberByIdQuery request, CancellationToken cancellationToken)
 {
+    cancellationToken.ThrowIfCancellationRequested();
     try
     {
         var queryOptions = new QueryOptions<TeamMember>
         {
             Filter = tm => tm.Id == request.Id,
             Include = t => t.Include(t => t.Image)
         };

         TeamMember? teamMember = await _repository.TeamMembersRepository.GetFirstOrDefaultAsync(queryOptions);
         …

Optionally, for full end-to-end support, consider extending your repository interface and implementations to accept and propagate CancellationToken.

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (3)

83-87: Consider re-fetching the updated TeamMember with includes after SaveChanges (shorter transaction, richer DTO mapping)

Right now, a separate query loads Image only when ImageId is set, and the mapping runs while the transaction is still open. A small refactor can:

  • shorten the transaction window,
  • avoid partial navigation loading risks (e.g., if TeamMemberDto mapping needs Category),
  • and keep the post-update read consistent with the committed state.

Suggested flow:

  1. Save changes, call scope.Complete() as soon as DB work is done.
  2. Re-fetch the updated TeamMember with Include for Image (and Category if your mapping needs it).
  3. Map and return.

Example (illustrative; outside the selected lines):

// after SaveChangesAsync() > 0:
scope.Complete();

var updated = await _repositoryWrapper.TeamMembersRepository.GetFirstOrDefaultAsync(
    new QueryOptions<TeamMember>
    {
        Filter = e => e.Id == request.Id,
        Include = q => q
            .Include(t => t.Image)
            .Include(t => t.Category)
    });

var resultDto = _mapper.Map<TeamMemberDto>(updated);
return Result.Ok(resultDto);

Also, confirm whether a missing Image entity should be tolerated or treated as NotFound — current logic silently returns null image in the DTO.


90-91: Make resultDto non-nullable; mapping here shouldn’t return null

_mapper.Map for a non-null source returns a non-null DTO. Prefer a non-nullable local to reflect intent.

-                TeamMemberDto? resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);
+                TeamMemberDto resultDto = _mapper.Map<TeamMember, TeamMemberDto>(entityToUpdate);

8-8: BlobStorageException import/catch likely stale — confirm necessity or remove

Given this handler no longer calls blob storage directly, the BlobStorageException catch (and using) might be dead code. If your AutoMapper resolver still uses IBlobService and may throw BlobStorageException, keeping the catch is fine — but consider tightening the error text to “team member image” for clarity. Otherwise, remove both to reduce coupling.

Option A — remove unused import and catch:

-using VictoryCenter.BLL.Exceptions.BlobStorageExceptions;
...
-        catch (BlobStorageException e)
-        {
-            return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}");
-        }

Option B — keep catch, adjust message:

-            return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}");
+            return Result.Fail<TeamMemberDto>($"Error with team member image: {e.Message}");

Please confirm whether the mapping pipeline can throw BlobStorageException in this path.

Also applies to: 98-101

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (3)

35-39: Existence check could live in FluentValidation (optional)

This category existence check works. Given this repo’s convention (validators handle input/business validation via ValidateAndThrowAsync early), consider moving the “CategoryId exists” check into the CreateTeamMemberCommand validator to keep handlers lean and consistent.

I can draft an async validator rule against CategoriesRepository if you’d like.


66-69: Guard against missing/invalid image (optional) and align behavior with GetImageById

If ImageId is provided but the image record is missing or lacks BlobName, the mapping may return null or trigger a resolver error. Decide whether creation should:

  • succeed with null Image (current behavior), or
  • fail fast with a clear error (similar to GetImageById).

If you choose to fail fast, consider:

-                    Image? imageResult = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
+                    Image? imageResult = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(
                         new QueryOptions<Image>
                         {
                             Filter = i => i.Id == entity.ImageId
                         });
 
-                    result.Image = _mapper.Map<ImageDTO>(imageResult);
+                    if (imageResult is null)
+                    {
+                        return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.NotFound(entity.ImageId, typeof(Image)));
+                    }
+                    if (string.IsNullOrEmpty(imageResult.BlobName))
+                    {
+                        return Result.Fail<TeamMemberDto>(ImageConstants.ImageDataNotAvailable);
+                    }
+                    result.Image = _mapper.Map<ImageDTO>(imageResult);

Alternatively, to keep mapping centralized, you could attach the image to the entity and map once after fetch.

Please confirm the intended business rule when a supplied ImageId points to a missing/invalid image.


82-85: Use centralized error formatter for blob errors

For consistency with other handlers and to keep error messaging uniform, use ErrorMessagesConstants.BlobStorageError(e.Message) instead of a custom string.

Apply:

-            return Result.Fail<TeamMemberDto>($"Error with user image: {e.Message}");
+            return Result.Fail<TeamMemberDto>(ErrorMessagesConstants.BlobStorageError(e.Message));
VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs (1)

3-4: Consider exposing Items as IReadOnlyList instead of T[].

Using IReadOnlyList provides flexibility for callers and avoids exposing a mutable array. It also plays nicer with most serializers.

Apply this diff:

-public record PaginationResult<T>(T[] Items, long TotalItemsCount)
+public record PaginationResult<T>(IReadOnlyList<T> Items, long TotalItemsCount)
     where T : class;

If needed, ensure the namespace includes the generic collections:

using System.Collections.Generic;
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (1)

83-86: Make CountAsync more flexible and cancellation-aware.

Two small improvements:

  • Accept a nullable filter so callers can count all entities without passing e => true.
  • Accept a CancellationToken and use AsNoTracking for consistency with read-only ops.

Apply this diff:

-    public Task<long> CountAsync(Expression<Func<T, bool>> filter)
-    {
-        return _dbContext.Set<T>().LongCountAsync(filter);
-    }
+    public Task<long> CountAsync(Expression<Func<T, bool>>? filter = null, CancellationToken cancellationToken = default)
+    {
+        var query = _dbContext.Set<T>().AsNoTracking();
+        if (filter != null)
+        {
+            query = query.Where(filter);
+        }
+        return query.LongCountAsync(cancellationToken);
+    }

Note: If not already globally available, add:

using System.Threading;
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (1)

23-23: Broaden CountAsync signature for null filter and cancellation support.

Allowing null (count all) and accepting a CancellationToken improves ergonomics and resilience, consistent with other async repository methods.

Apply this diff:

-    Task<long> CountAsync(Expression<Func<T, bool>> filter);
+    Task<long> CountAsync(Expression<Func<T, bool>>? filter = null, CancellationToken cancellationToken = default);

If needed, add:

using System.Threading;
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/TeamMembers/GetFiltered/GetFilteredTeamMembersTests.cs (1)

29-35: Good adaptation of the test to the paginated contract.

Deserialization to PaginationResult and validating Items + TotalItemsCount are on point.

You can slightly strengthen the assertion to reflect pagination invariants.

Apply this diff:

         Assert.Equal(HttpStatusCode.OK, response.StatusCode);
         Assert.NotNull(responseContent);
         Assert.NotEmpty(responseContent.Items);
         Assert.True(responseContent.TotalItemsCount > 0);
+        Assert.True(responseContent.TotalItemsCount >= responseContent.Items.Length);
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (1)

3-14: Consider adding serialization and standard exception constructors to align with .NET guidelines

Adding [Serializable] and a protected serialization constructor improves compatibility (e.g., logging, cross-boundary scenarios) and satisfies common analyzers (CA1032/CA1064). A parameterless protected ctor is also conventional for base exception types.

Apply this diff:

 public abstract class BlobStorageException : Exception
 {
+    protected BlobStorageException()
+    {
+    }
+
     protected BlobStorageException(string message)
         : base(message)
     {
     }
 
     protected BlobStorageException(string message, Exception innerException)
         : base(message, innerException)
     {
     }
+
+    protected BlobStorageException(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+        : base(info, context)
+    {
+    }
 }

Additionally, consider decorating the class with the attribute:

- public abstract class BlobStorageException : Exception
+ [Serializable]
+ public abstract class BlobStorageException : Exception
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1)

3-12: Add inner-exception overload and serialization support (preserving FileName)

This makes the exception more informative and analyzer-friendly when chaining exceptions and serializing. It also preserves FileName during serialization.

Apply this diff:

-public class BlobNotFoundException : BlobStorageException
+ [Serializable]
+ public class BlobNotFoundException : BlobStorageException
 {
     public BlobNotFoundException(string fileName, string message)
         : base(message)
     {
         FileName = fileName;
     }
 
+    public BlobNotFoundException(string fileName, string message, Exception innerException)
+        : base(message, innerException)
+    {
+        FileName = fileName;
+    }
+
+    protected BlobNotFoundException(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+        : base(info, context)
+    {
+        FileName = info.GetString(nameof(FileName))!;
+    }
+
     public string FileName { get; }
+
+    public override void GetObjectData(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+    {
+        if (info is null) throw new ArgumentNullException(nameof(info));
+        info.AddValue(nameof(FileName), FileName);
+        base.GetObjectData(info, context);
+    }
 }
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1)

3-14: Optional: add serialization constructor and [Serializable] for completeness

Even though responses are now URL-based, this exception likely still appears on uploads. Adding the serialization constructor and attribute keeps consistency with the exception hierarchy.

Apply this diff:

-public class InvalidBase64FormatException : BlobStorageException
+ [Serializable]
+ public class InvalidBase64FormatException : BlobStorageException
 {
     public InvalidBase64FormatException(string message)
         : base(message)
     {
     }
 
     public InvalidBase64FormatException(string message, Exception innerException)
         : base(message, innerException)
     {
     }
+
+    protected InvalidBase64FormatException(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+        : base(info, context)
+    {
+    }
 }
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1)

3-18: Preserve FileName during serialization and add [Serializable]

For parity with BlobNotFoundException and to keep diagnostic context, serialize FileName and add the serialization constructor.

Apply this diff:

-public class ImageProcessingException : BlobStorageException
+ [Serializable]
+ public class ImageProcessingException : BlobStorageException
 {
     public ImageProcessingException(string fileName, string message)
         : base(message)
     {
         FileName = fileName;
     }
 
     public ImageProcessingException(string fileName, string message, Exception innerException)
         : base(message, innerException)
     {
         FileName = fileName;
     }
 
     public string FileName { get; }
+
+    protected ImageProcessingException(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+        : base(info, context)
+    {
+        FileName = info.GetString(nameof(FileName))!;
+    }
+
+    public override void GetObjectData(
+        System.Runtime.Serialization.SerializationInfo info,
+        System.Runtime.Serialization.StreamingContext context)
+    {
+        if (info is null) throw new ArgumentNullException(nameof(info));
+        info.AddValue(nameof(FileName), FileName);
+        base.GetObjectData(info, context);
+    }
 }
VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (3)

57-57: Remove unused variable from repository Update call

The EntityEntry result variable isn’t used and can be dropped for clarity.

Apply this diff:

-            EntityEntry<Image> result = _repositoryWrapper.ImageRepository.Update(imageEntity);
+            _repositoryWrapper.ImageRepository.Update(imageEntity);

54-55: Name clarity: previousType → previousMimeType

Improves readability and reduces ambiguity.

Apply this diff:

-            var previousType = imageEntity.MimeType;
+            var previousMimeType = imageEntity.MimeType;
@@
-                previousType,
+                previousMimeType,

Also applies to: 66-66


64-70: Pass CancellationToken to blob/storage operations if supported

To make the handler truly cancellable, propagate cancellationToken to SaveChangesAsync and BlobService calls if their signatures allow it.

If available, prefer:

-            var updatedBlobName = await _blobService.UpdateFileInStorageAsync(
+            var updatedBlobName = await _blobService.UpdateFileInStorageAsync(
                 imageEntity.BlobName,
-                previousType,
+                previousMimeType,
                 request.UpdateImageDto.Base64!,
                 imageEntity.BlobName,
-                request.UpdateImageDto.MimeType!);
+                request.UpdateImageDto.MimeType!,
+                cancellationToken);
@@
-            await _repositoryWrapper.SaveChangesAsync();
+            await _repositoryWrapper.SaveChangesAsync(cancellationToken);

If these APIs don’t currently accept a CancellationToken, consider adding overloads.

VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (1)

46-48: Consider null-checking the mapped result.

While the mapper configuration should handle the mapping correctly, the result is marked as nullable (ImageDTO?). For defensive programming, consider verifying the mapping succeeded before returning.

 ImageDTO? result = _mapper.Map<ImageDTO>(image);
+
+if (result is null)
+{
+    return Result.Fail<ImageDTO>(ImageConstants.FailedToMapImage);
+}

 return Result.Ok(result);
VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (1)

39-54: Good transaction management, but consider error handling for blob deletion failures.

The transaction scope usage is correct, and the database operations are properly wrapped. However, if blob deletion fails after the database commit, you'll have orphaned files in storage. Consider either:

  1. Moving blob deletion before database operations (if idempotent)
  2. Implementing compensating logic for cleanup

For better resilience against partial failures, consider implementing a two-phase approach or eventual consistency pattern for blob cleanup. You could also explore using a background job to clean up orphaned blobs if the immediate deletion fails.

VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)

38-56: Consider the transaction boundary for blob storage operations.

The blob storage operation (Line 51) is inside the transaction scope but appears to be a filesystem operation that won't participate in the database transaction. If blob storage fails, the transaction will roll back the database changes, which is good. However, if the transaction fails to complete after blob storage succeeds, you'll have orphaned files.

Consider implementing a cleanup mechanism for orphaned blobs or using a two-phase commit pattern if your blob storage supports it. Alternatively, you could track blob cleanup requirements in a separate table within the same transaction.

VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)

36-39: Avoid leaking raw exception messages; use a consistent, user-safe message.

Prefer a stable error message to keep logs consistent and avoid surfacing internals.

Apply this diff:

-            throw new BlobFileSystemException(_blobEnv.FullPath, ex.Message, ex);
+            throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.FailToSaveImageInStorage, ex);

50-65: Sanitize URL construction: trim slashes and URL-encode the file name.

Minor hardening: avoid double slashes if ImagesSubPath is configured with slashes and ensure the file name is URL-safe.

Apply this diff:

     public string GetFileUrl(string name, string mimeType)
     {
         ValidateFileName(name);

         var extension = GetExtensionFromMimeType(mimeType);
-        var fileName = $"{name}.{extension}";
-        HttpRequest? request = _httpContextAccessor.HttpContext?.Request;
+        var fileName = $"{name}.{extension}";
+        var subPath = _blobEnv.ImagesSubPath.Trim('/');
+        HttpRequest? request = _httpContextAccessor.HttpContext?.Request;

         if (request == null)
         {
-            throw new InvalidOperationException("HttpContext is not available.");
+            throw new InvalidOperationException("HttpContext is not available.");
         }

-        var baseUrl = $"{request.Scheme}://{request.Host}";
-        return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
+        var baseUrl = $"{request.Scheme}://{request.Host}";
+        var safeFileName = Uri.EscapeDataString(fileName);
+        return $"{baseUrl}/{subPath}/{safeFileName}";
     }

58-61: Consider a graceful fallback when HttpContext is unavailable.

If this runs outside an HTTP request (jobs, background services), throwing may be undesirable. A relative URL fallback keeps the system functional.

Apply this diff if a relative URL is acceptable without HttpContext:

-        if (request == null)
-        {
-            throw new InvalidOperationException("HttpContext is not available.");
-        }
+        if (request == null)
+        {
+            // Fallback to relative URL when HttpContext is not available
+            return $"/{subPath}/{Uri.EscapeDataString(fileName)}";
+        }

If absolute URLs are required, consider introducing a BaseUrl in BlobEnvironmentVariables and falling back to it when HttpContext is null.


94-122: Preserve inner exception when rethrowing InvalidBase64FormatException.

Keeping the inner exception materially improves diagnostics without changing behavior.

Apply this diff:

         catch (Exception ex) when (ex is not InvalidBase64FormatException)
         {
-            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64);
+            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex);
         }

173-181: Tighten file name validation logic (IndexOfAny and explicit separators).

Using IndexOfAny is more idiomatic and faster than Any(name.Contains). Explicitly disallowing path separators makes behavior OS-agnostic and URL-friendly.

Apply this diff:

     private void ValidateFileName(string name)
     {
-        if (string.IsNullOrWhiteSpace(name)
-            || name.Contains("..")
-            || Path.GetInvalidFileNameChars().Any(name.Contains))
+        if (string.IsNullOrWhiteSpace(name)
+            || name.Contains("..")
+            || name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
+            || name.Contains('/') || name.Contains('\\'))
         {
             throw new BlobFileNameException(name, ImageConstants.CantGetFile(name));
         }
     }
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (4)

36-42: Use a realistic URL in the test double.

The Url field currently holds base64 (“dGVzdA==”). Switching to a plausible URL better reflects the new model and improves test readability.

Apply this diff:

     private readonly ImageDTO _testImageDto = new()
     {
         Id = 1,
         BlobName = "testblob",
         MimeType = "image/png",
-        Url = "dGVzdA=="
+        Url = "http://localhost/images/testblob.png"
     };

58-60: Remove unused local variable.

fileName is declared but never used.

Apply this diff:

-        var fileName = "testblob";
         var fileWithExtension = "testblob.png";

160-186: Rename test for accuracy (it throws BlobFileSystemException, not IOException).

Minor clarity improvement for future readers.

Apply this diff:

-    public async Task Handle_ThrowsIOException_ShouldReturnFileCreatingFail()
+    public async Task Handle_FileSystemError_ShouldReturnFileCreatingFail()

131-134: Consider adding a unit test for InvalidBase64 flow.

A test where the blob service throws InvalidBase64FormatException would complement coverage for the new base64 parsing path.

If you’d like, I can add a new test method that configures SaveFileInStorageAsync to throw InvalidBase64FormatException and asserts the handler returns a Failure with an appropriate error message.

Also applies to: 141-143, 155-158

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 36eebcc and 9d9203d.

📒 Files selected for processing (25)
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (3 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (4 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (3 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Payment/WayForPay/WayForPayPaymentCommandHandler.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (4 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (5 hunks)
  • VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (4 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs (4 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (4 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersQuery.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (3 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4 hunks)
  • VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (1 hunks)
  • VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/TeamMembers/GetFiltered/GetFilteredTeamMembersTests.cs (2 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (8 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (6 hunks)
💤 Files with no reviewable changes (1)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorage.cs
✅ Files skipped from review due to trivial changes (1)
  • VictoryCenter/VictoryCenter.BLL/Commands/Payment/WayForPay/WayForPayPaymentCommandHandler.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • VictoryCenter/VictoryCenter.BLL/Queries/Images/GetByName/GetImageByNameHandler.cs
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-20T18:22:51.823Z
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.

Applied to files:

  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs
📚 Learning: 2025-06-17T20:32:16.009Z
Learnt from: VladimirSushinsky
PR: ita-social-projects/VictoryCenter-Back#114
File: VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/TeamMembersSeeder/TeamMemberSeeder.cs:20-24
Timestamp: 2025-06-17T20:32:16.009Z
Learning: In the VictoryCenter test seeder for TeamMember entities, the last category in the categories list is intentionally excluded from having team members assigned to it (using `categories[i % (categories.Count - 1)].Id`). This design ensures that the last category remains available for delete tests without foreign key constraint violations, as delete operations require categories with no related team members.

Applied to files:

  • VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs
🧬 Code Graph Analysis (16)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (2)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (1)
  • Task (29-50)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (5)
  • Task (10-10)
  • Task (12-12)
  • Task (14-14)
  • Task (20-21)
  • Task (23-23)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (2)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
  • TransactionScope (18-18)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
  • TransactionScope (40-43)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMemberById.cs (2)
  • TeamMemberDto (82-88)
  • TeamMemberDto (90-93)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (5)
  • Task (10-10)
  • Task (12-12)
  • Task (14-14)
  • Task (20-21)
  • Task (23-23)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (5)
  • Task (21-35)
  • Task (37-48)
  • Task (50-54)
  • Task (66-81)
  • Task (83-86)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
  • Task (35-38)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (4)
VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (3)
  • ErrorMessagesConstants (3-66)
  • NotFound (5-8)
  • NotFound (10-18)
VictoryCenter/VictoryCenter.DAL/Entities/Category.cs (1)
  • Category (3-14)
VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
  • QueryOptions (6-15)
VictoryCenter/VictoryCenter.DAL/Entities/TeamMember.cs (1)
  • TeamMember (5-30)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (8)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (1)
  • Task (24-61)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)
  • Task (30-68)
VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (1)
  • Task (27-54)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)
  • Task (28-53)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
  • Task (22-40)
  • Task (42-48)
  • Task (67-73)
  • Task (136-150)
  • Task (152-171)
VictoryCenter/VictoryCenter.DAL/Entities/Category.cs (1)
  • Category (3-14)
VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
  • QueryOptions (6-15)
VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (3)
  • ErrorMessagesConstants (3-66)
  • NotFound (5-8)
  • NotFound (10-18)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Delete/DeleteImageHandler.cs (6)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
  • ImageRepository (8-14)
  • ImageRepository (10-13)
VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
  • QueryOptions (6-15)
VictoryCenter/VictoryCenter.BLL/Constants/ErrorMessagesConstants.cs (4)
  • ErrorMessagesConstants (3-66)
  • NotFound (5-8)
  • NotFound (10-18)
  • BlobStorageError (62-65)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
  • TransactionScope (18-18)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
  • TransactionScope (40-43)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)
  • DeleteFileInStorage (75-92)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/TeamMembers/TeamMembersRepository.cs (2)
  • TeamMembersRepository (8-14)
  • TeamMembersRepository (10-13)
VictoryCenter/VictoryCenter.DAL/Entities/TeamMember.cs (1)
  • TeamMember (5-30)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMemberById.cs (2)
  • TeamMember (69-75)
  • TeamMember (77-80)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (12)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (2)
  • Task (162-186)
  • GetExtensionFromMimeType (188-198)
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeder/IntegrationTestsDatabaseSeeder.cs (1)
  • Task (19-28)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (2)
  • Task (229-248)
  • GetExtensionFromMimeType (250-260)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (2)
  • Task (146-173)
  • GetExtensionFromMimeType (175-185)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
  • GetExtensionFromMimeType (168-178)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
  • BlobFileSystemException (3-18)
  • BlobFileSystemException (5-9)
  • BlobFileSystemException (11-15)
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
  • GetFileUrl (6-6)
  • DeleteFileInStorage (8-8)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
  • InvalidBase64FormatException (3-14)
  • InvalidBase64FormatException (5-8)
  • InvalidBase64FormatException (10-13)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
  • ImageConstants (3-41)
  • CantGetFile (37-40)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
  • ImageProcessingException (3-18)
  • ImageProcessingException (5-9)
  • ImageProcessingException (11-15)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
  • BlobFileNameException (3-18)
  • BlobFileNameException (5-9)
  • BlobFileNameException (11-15)
VictoryCenter/VictoryCenter.BLL/Queries/Images/GetById/GetImageByIdHandler.cs (2)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
  • ImageRepository (8-14)
  • ImageRepository (10-13)
VictoryCenter/VictoryCenter.DAL/Repositories/Options/QueryOptions.cs (1)
  • QueryOptions (6-15)
VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (3)
VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryWrapper.cs (1)
  • TransactionScope (18-18)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryWrapper.cs (1)
  • TransactionScope (40-43)
VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Media/ImageRepository.cs (2)
  • ImageRepository (8-14)
  • ImageRepository (10-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and analyze
🔇 Additional comments (30)
VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (1)

7-7: LGTM: Targeted BlobStorage exception namespace import

The dedicated BlobStorageExceptions namespace aligns with the new error model and the catch below.

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)

24-24: Constructor dependency removal looks good — double-check DI and tests

Dropping IBlobService from the handler’s constructor aligns with the PR’s image URL approach. Please ensure DI registrations and any tests constructing this handler are updated accordingly.


60-61: Good, consistent NotFound handling for Category

Using ErrorMessagesConstants.NotFound for missing Category keeps responses uniform across the codebase.

VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs (4)

8-8: Import aligns with ImageDTO usage — LGTM

The added DTOs.Images import is appropriate now that the handler assigns ImageDTO on the result.


10-10: Exception namespace update looks correct

The BlobStorageExceptions namespace aligns with the refactor across the codebase.


43-44: Standardized NotFound message — LGTM

This matches ErrorMessagesConstants.NotFound(...) usage in other handlers.


23-28: DI coverage verified for IBlobService and BlobToUrlResolver
IBlobService remains registered in DI and the AutoMapper profile includes the BlobToUrlResolver mapping:

  • ServicesConfiguration.cs (lines 300 & 306): services.AddScoped<IBlobService, BlobService>()
  • ImagesProfile.cs (line 14): CreateMap<Image, ImageDTO>().ForMember(d ⇒ d.Url, o ⇒ o.MapFrom<BlobToUrlResolver>())
VictoryCenter/VictoryCenter.BLL/DTOs/Common/PaginationResult.cs (1)

3-4: Solid, minimal DTO for pagination.

The simple record with Items + TotalItemsCount aligns well with the new pagination flow and LongCount usage.

VictoryCenter/VictoryCenter.DAL/Repositories/Realizations/Base/RepositoryBase.cs (1)

83-86: CountAsync implementation is correct and consistent with EF Core.

Delegating to LongCountAsync(filter) matches the new interface and supports the pagination total count cleanly.

VictoryCenter/VictoryCenter.DAL/Repositories/Interfaces/Base/IRepositoryBase.cs (1)

23-23: Interface addition looks good and supports the new pagination flow.

Introducing CountAsync on the repository aligns with the handler’s need to compute TotalItemsCount.

VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersQuery.cs (1)

8-10: Return type change to PaginationResult is appropriate.

This keeps the public contract aligned with the handler and tests, enabling efficient pagination without changing the query shape.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/GetTeamMembers.cs (6)

1-1: Added necessary import for Expression support.

The System.Linq.Expressions import is correctly added to support the new CountAsync method mock setup that uses Expression<Func<TeamMember, bool>>.


63-67: Updated test assertions to validate paginated response structure.

The test assertions have been correctly updated to work with the new paginated response structure:

  • Accessing filtered results through result.Value.Items
  • Validating total count through result.Value.TotalItemsCount
  • Maintaining proper comparison logic for both filtered and total datasets

100-102: Consistent paginated assertions across filter tests.

All filter-based tests now consistently validate the paginated response structure, ensuring both the filtered items and total count are properly asserted.


136-138: Filter by category test properly validates pagination.

The category filter test correctly asserts the paginated structure while maintaining the filtering logic validation.


173-175: Combined filter test maintains proper pagination validation.

The status and category combined filter test appropriately validates both the filtered results and total count in the paginated response.


276-277: Added repository mock setup for counting functionality.

The CountAsync mock setup correctly returns the total count of the team members list, enabling the pagination total count functionality in the tests.

VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetByFilters/GetTeamMembersByFiltersHandler.cs (6)

6-6: Added pagination DTO import.

The import for VictoryCenter.BLL.DTOs.Common is correctly added to support the new PaginationResult<T> return type.


16-16: Updated handler interface to support pagination.

The class declaration has been correctly updated to implement IRequestHandler<GetTeamMembersByFiltersQuery, Result<PaginationResult<TeamMemberDto>>>, aligning with the new paginated response structure.


29-29: Method signature updated for paginated results.

The Handle method signature correctly returns Task<Result<PaginationResult<TeamMemberDto>>> to support the new pagination functionality.


47-47: Added total count retrieval for pagination.

The handler now correctly retrieves the total count of items matching the filter criteria using CountAsync, which is essential for proper pagination metadata.


49-49: Proper pagination result construction.

The return statement correctly constructs a PaginationResult<TeamMemberDto> with the mapped DTOs and total count, using the collection expression syntax for the items array.


47-47: No action required on CountAsync return type

I’ve verified that:

  • IRepositoryBase declares Task<long> CountAsync(Expression<Func<T, bool>> filter).
  • RepositoryBase implements it by returning _dbContext.Set<T>().LongCountAsync(filter).
  • The handler correctly awaits the Task<long> to obtain a long.

All definitions and usages are consistent—no changes needed here.

VictoryCenter/VictoryCenter.BLL/Commands/Images/Update/UpdateImageHandler.cs (1)

36-50: Transactional flow looks solid with TransactionScopeAsyncFlowOption.Enabled

Validation precedes data access; not-found early-return is clean; and exceptions translate to a Result with consistent messages. With the SaveChanges fix above, the DB and blob operations will remain consistent.

VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)

1-18: LGTM! Well-structured exception class for blob file name errors.

The implementation follows proper exception handling patterns with appropriate constructors and property initialization. The class correctly inherits from BlobStorageException and provides clear semantics for blob file name-related errors.

VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1)

1-18: LGTM! Clean exception implementation for filesystem-related blob errors.

The class follows the established pattern with proper constructor chaining and exposes the filesystem path for diagnostic purposes. This will be helpful for debugging storage-related issues.

VictoryCenter/VictoryCenter.BLL/Commands/Images/Create/CreateImageHandler.cs (1)

42-42: Good addition of timestamp tracking.

Setting CreatedAt to UTC ensures consistent timestamp handling across different timezones.

VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (2)

136-150: LGTM on file creation path and error mapping.

Validate-then-write with a clear ImageProcessingException on failure aligns with the new, non-encrypted flow.


152-171: LGTM on read path and exception mapping.

Clean separation of not-found vs. read failure with BlobNotFoundException and ImageProcessingException.

VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs (1)

119-125: LGTM: Validation failure path is asserted via FluentResults.

Asserting for “required/cannot be null” aligns with FluentValidation surface.

Comment on lines +88 to +89
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase +
ex.Message);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid leaking database exception details to clients

Returning DbUpdateException.Message can expose internal DB details. Prefer a generic, user-safe message and log the exception server-side.

Apply:

-            return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase +
-                                              ex.Message);
+            return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase);

Follow-up: If there’s a centralized logger in the handler pipeline, rely on it; otherwise consider logging ex with context here.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase +
ex.Message);
return Result.Fail<TeamMemberDto>(TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase);
🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Create/CreateTeamMemberHandler.cs
around lines 88-89, the handler returns the raw DbUpdateException.Message to the
client; replace that with a generic, user-safe failure message (e.g.
TeamMemberConstants.FailedToCreateNewTeamMemberInTheDatabase) and do not append
ex.Message to the Result.Fail return value. Instead log the full exception
server-side (use the centralized pipeline logger if available, otherwise call a
local logger.LogError(ex, "Failed creating team member for {TeamMemberInfo}")
with relevant context) before returning the generic failure result.

Comment on lines 67 to 73
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
{
ValidateFileName(newBlobName);
DeleteFileInStorage(previousBlobName, previousMimeType);
await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
return newBlobName;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid data loss: save new blob before deleting the old one.

Currently, the old file is deleted first. If saving the new file fails, you’ve lost the original asset. Reverse the order to be fail-safe.

Apply this diff:

     public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
     {
         ValidateFileName(newBlobName);
-        DeleteFileInStorage(previousBlobName, previousMimeType);
-        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
-        return newBlobName;
+        // Save first to avoid data loss, then delete the previous blob
+        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        DeleteFileInStorage(previousBlobName, previousMimeType);
+        return newBlobName;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
{
ValidateFileName(newBlobName);
DeleteFileInStorage(previousBlobName, previousMimeType);
await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
return newBlobName;
}
public async Task<string> UpdateFileInStorageAsync(
string previousBlobName,
string previousMimeType,
string base64Format,
string newBlobName,
string mimeType)
{
ValidateFileName(newBlobName);
// Save first to avoid data loss, then delete the previous blob
await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
DeleteFileInStorage(previousBlobName, previousMimeType);
return newBlobName;
}
🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs around
lines 67-73, the method deletes the previous blob before saving the new one
which risks data loss; change the flow to ValidateFileName(newBlobName), then
await SaveFileInStorageAsync(...) to persist the new blob first, and only after
a successful save call DeleteFileInStorage(previousBlobName, previousMimeType);
additionally wrap the save call in error handling so any exception prevents
deletion (and log/report the error) to ensure the original asset remains if the
new upload fails.

@taraskibysh taraskibysh changed the title Feature/issue 309 Feature/issue 309 changing photo logic from base64 to url approach Aug 17, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)

59-77: Re-evaluate where to validate empty BlobName (creation vs retrieval)

This test asserts 400 on GET when BlobName is empty by inserting an invalid record directly into the DB. It’s generally cleaner to prevent such invalid state at creation time rather than at retrieval. Pick one policy and align tests and controller accordingly.

  • Policy A (preferred): Disallow creating images with empty BlobName (return 400/422 on POST). Remove or repurpose this GET test.
  • Policy B: If API contract specifies 400 on GET for empty BlobName, ensure controller explicitly returns 400 with a clear error payload and document it. Add an assertion on the error message/code, not just the status.

I can update the tests and (if needed) the controller to enforce either policy—what’s your preference?

VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)

118-128: Extract GetExtensionFromMimeType into a shared test helper to avoid duplication

This helper is now present in multiple test files. Centralize it to a single utility to keep behavior consistent with production mapping.

Proposed shared helper (new file in the test project):

namespace VictoryCenter.IntegrationTests.TestUtils;

public static class ImageTestHelper
{
    public static string GetExtensionFromMimeType(string mimeType)
    {
        return mimeType.ToLowerInvariant() switch
        {
            "image/jpeg" => "jpg",
            "image/jpg"  => "jpg",
            "image/png"  => "png",
            "image/webp" => "webp",
            _            => "jpg"
        };
    }
}

Then replace local calls with ImageTestHelper.GetExtensionFromMimeType(mimeType).

🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs (2)

3-9: Make the seed model immutable and explicit.

Given this type is only for seeding, using init-only and required members (C# 11+) avoids accidental mutation and null-forgiveness. It also documents intent.

Apply this diff:

-public class ImageEntityForSeed
-{
-    public int Id { get; set; }
-    public string BlobName { get; set; } = default!;
-    public string MimeType { get; set; } = default!;
-    public string Base64 { get; set; } = default!;
-}
+public sealed record ImageEntityForSeed
+{
+    public required int Id { get; init; }
+    public required string BlobName { get; init; }
+    public required string MimeType { get; init; }
+    // Note: this currently contains a data URI (e.g., "data:image/jpeg;base64,...")
+    public required string Base64 { get; init; }
+}

8-8: Clarify representation: Base64 vs Data URI.

The Base64 values you pass include a data URI prefix (e.g., "data:image/jpeg;base64,..."). Consider renaming to DataUri or documenting this explicitly to prevent misuse later.

VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs (4)

13-28: Move large inline Base64 blobs to test assets for maintainability.

Embedding multi-hundred-KB Base64 strings in code hurts readability, diffs, and repository size. Prefer small fixture images (e.g., 1x1 or tiny thumbnails) stored under a test assets folder and load bytes at runtime.

Example approach (outside this hunk):

  • Place files under VictoryCenter.IntegrationTests/TestAssets/Images/testname1.jpg.
  • Load and convert to Base64 only for seeding.
// Example helper
static string ToDataUri(string path, string mime) =>
    $"data:{mime};base64,{Convert.ToBase64String(File.ReadAllBytes(path))}";

If you want, I can provide a follow-up patch to wire this into the seeder.


47-53: Optional: parallelize uploads for speed (tiny dataset, but scalable).

If the storage backend allows, you can upload seeds concurrently. Keep sequential if the backend throttles or if ordering matters.

Example (outside this hunk to avoid diff conflicts with the guard above):

var uploadTasks = Images
    .Where(i => !string.IsNullOrWhiteSpace(i.Base64))
    .Select(i => _blobService.SaveFileInStorageAsync(i.Base64, i.BlobName, i.MimeType));

await Task.WhenAll(uploadTasks);

11-11: Prefer IReadOnlyList for immutable seed data.

The seed collection isn’t modified; using IReadOnlyList documents intent and prevents accidental mutation.

-private static readonly List<ImageEntityForSeed> Images =
+private static readonly IReadOnlyList<ImageEntityForSeed> Images =

49-53: Strengthen the null/empty guard; no need to strip data-URI manually

I’ve confirmed that IBlobService.SaveFileInStorageAsync accepts both raw Base64 and full data-URI formats—the implementation’s ConvertBase64ToBytes helper automatically splits out the prefix when a comma is present . The integration tests even seed images using "data:image/...;base64,...", while unit tests pass raw Base64, and both work correctly.

The only change still worth making is tightening the guard to skip empty or whitespace-only strings:

• File: VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs, around lines 49–53
• Update the null check to also exclude empty/whitespace strings

-            if (image.Base64 != null)
+            if (!string.IsNullOrWhiteSpace(image.Base64))
             {
                 await _blobService.SaveFileInStorageAsync(image.Base64, image.BlobName, image.MimeType);
             }
VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1)

8-8: Good isolation intent; consider per-run subpath to avoid cross-test collisions

Setting ImagesSubPath to "IntegrationTest" keeps test artifacts away from real data. To harden this further (parallel runs, flaky cleanups), generate a unique subfolder per run (e.g., IntegrationTest/{GUID or timestamp}) via test-time configuration override rather than static JSON. This also lets you guard deletions safely in teardown.

Would you like me to sketch a factory override that sets a per-run ImagesSubPath in your VictoryCenterWebApplicationFactory?

VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1)

35-46: Guard against null seed record to avoid intermittent NREs

FirstOrDefaultAsync can return null and cause a NullReference when dereferencing image.Id. Assert non-null before use.

-        Image? image = await _fixture.DbContext.Images.FirstOrDefaultAsync();
-        var id = image.Id;
+        Image? image = await _fixture.DbContext.Images.FirstOrDefaultAsync();
+        Assert.NotNull(image);
+        var id = image!.Id;
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)

84-116: Nice coverage for MIME variants; tighten a couple of assertions

Good parameterization across MIME types and on-disk verification. Two small improvements:

  • Also assert responseContext.Url is absolute and not empty.
  • Keep comments in English for consistency with the rest of the suite.
-        // Перевіряємо що файл створено з правильним розширенням
+        // Verify that the file is created with the correct extension
         string expectedExtension = GetExtensionFromMimeType(mimeType);
         string filePath = Path.Combine(_fixture.BlobEnvironmentVariables.FullPath, $"{responseContext.BlobName}.{expectedExtension}");
         Assert.True(File.Exists(filePath));

-        // Перевіряємо що URL містить правильне розширення
-        Assert.Contains($".{expectedExtension}", responseContext.Url);
+        // Verify that the URL contains the correct extension and is absolute
+        Assert.False(string.IsNullOrWhiteSpace(responseContext.Url));
+        Assert.Contains($".{expectedExtension}", responseContext.Url);
+        Assert.True(Uri.TryCreate(responseContext.Url, UriKind.Absolute, out _));
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9d9203d and 1ae6abb.

📒 Files selected for processing (9)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/DbFixture/IntegrationTestDbFixture.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (2 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs (3 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs (1 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs (2 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/appsettings.IntegrationTests.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Delete/DeleteImageTests.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Update/UpdateImageTest.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
🧰 Additional context used
🧬 Code graph analysis (2)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetById/GetImageByIdTest.cs (3)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (5)
  • Fact (32-46)
  • Fact (48-57)
  • Fact (72-89)
  • Task (25-28)
  • Task (30-30)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Images/GetImageById.cs (3)
  • Fact (40-67)
  • Fact (69-92)
  • Fact (94-125)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)
  • Task (22-40)
  • Task (42-48)
  • Task (67-73)
  • Task (136-150)
  • Task (152-171)
VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/Create/CreateImageTests.cs (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (6)
  • Task (22-40)
  • Task (42-48)
  • Task (67-73)
  • Task (136-150)
  • Task (152-171)
  • GetExtensionFromMimeType (124-134)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze
🔇 Additional comments (1)
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImageEntityForSeed.cs (1)

3-9: LGTM on scope and simplicity.

A minimal, seed-only DTO is appropriate here and keeps the app code decoupled from test data shape.

Comment on lines +13 to 20
new ImageEntityForSeed
{
Id = 1,
BlobName = "testname1",
Base64 =
"data:image/jpeg;base64,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",
MimeType = "image/jpg"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix MIME type mismatch (image/jpg → image/jpeg).

Your data URIs start with "data:image/jpeg;base64,...", but MimeType is set to "image/jpg". Some servers/clients rely on the exact content type; the non-standard "image/jpg" can lead to incorrect headers and rendering issues.

Apply this diff in both seed items:

-            MimeType = "image/jpg"
+            MimeType = "image/jpeg"

Also applies to: 21-28

🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs
around lines 13-20 and 21-28, the ImageEntityForSeed entries set MimeType =
"image/jpg" while the Base64 data URIs begin with "data:image/jpeg;base64,...";
update both seed items to use the standard MIME type "image/jpeg" (replace
"image/jpg" with "image/jpeg") so the MIME matches the data URI and avoids
client/server rendering issues.

Comment on lines 55 to 60
var entities = Images.Select(i => new Image
{
Id = i.Id,
BlobName = i.BlobName,
Base64 = i.Base64,
MimeType = i.MimeType
}).ToList();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Explicit Id assignment may conflict with DB identity strategy—please verify.

If Image.Id is configured as ValueGeneratedOnAdd/Identity, setting Id explicitly can fail on some providers unless identity insert is enabled. It’s fine if the model is configured for explicit keys in tests.

Run:

If Identity is enforced, consider omitting Id here and mapping by BlobName, or temporarily enabling identity insert in the test setup.


🏁 Script executed:

#!/bin/bash
# Inspect Image entity configuration and annotations
rg -nP --type=cs -C3 $'class\\s+Image\\b'
rg -nP --type=cs -C3 'builder\\.(Entity<\\s*Image\\s*>|Property\\(|HasKey\\()'
rg -nP --type=cs -C3 'DatabaseGenerated|ValueGenerated|ValueGeneratedOn(Add|Never)'

Length of output: 3036


Address explicit Id assignment in the Image seeder

I noticed that in VictoryCenter.DAL/Data/EntityTypeConfigurations/ImageConfig.cs, the Id property is configured with .ValueGeneratedOnAdd(), so the database will generate its value on insert. In your integration‐test seeder you’re explicitly setting Id on each Image instance, which will cause EF Core to include the Id column in the INSERT and fail under a SQL identity strategy unless IDENTITY_INSERT is enabled.

To resolve this, choose one of the following approaches:

• Remove explicit Id assignment in the seeder
– Let the database generate the key. After calling SaveChanges(), EF Core will populate each Image.Id automatically.
– Update your test assertions to refer to another stable property (e.g. BlobName) or read back the generated Id value from the context.

• Enable IDENTITY_INSERT around your seed operation
– Open a connection and toggle IDENTITY_INSERT on the target table, then seed and toggle it off. For example:
csharp await using var conn = context.Database.GetDbConnection(); await conn.OpenAsync(); await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT media.images ON"); context.Images.AddRange(entities); await context.SaveChangesAsync(); await context.Database.ExecuteSqlRawAsync("SET IDENTITY_INSERT media.images OFF");
– Wrap the above in a transaction to ensure cleanup on failures.

Please apply one of these fixes so that your integration tests can seed Image records reliably under the existing identity‐generation configuration.

🤖 Prompt for AI Agents
In
VictoryCenter/VictoryCenter.IntegrationTests/Utils/Seeders/Images/ImagesSeeder.cs
around lines 55-60 you are explicitly assigning Ids on Image instances even
though Image.Id is configured with .ValueGeneratedOnAdd(); remove the explicit
Id assignment and let the database generate keys (call
SaveChanges/SaveChangesAsync so EF populates Image.Id) and update tests to
reference BlobName or the generated Id read from the context, or alternatively
implement IDENTITY_INSERT around the seed operation (open connection, SET
IDENTITY_INSERT ON, add entities, SaveChanges, SET IDENTITY_INSERT OFF) wrapped
in a transaction to ensure cleanup on failure—pick one of these two approaches
and apply it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comments that explain what every type of exceptions is responsible for.

@roman-stozhuk roman-stozhuk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall - LGTM!

return $"File not found: {filePath}";
}

public static string CantGetFile(string name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give this method a better name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

public static readonly string FailedToReadOrDecryptFile = "Failed to read or decrypt file.";
public static readonly string UnexpectedBlobReadError = "Unexpected error during file retrieval.";
public static readonly string ImageDataNotAvailable = "Image data not available";
public static readonly string FailedToSaveImage = "Failed to save the image.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract this to a common constant. You can take a look at our pr: ita-social-projects/VictoryCenter-Client#52

Comment thread VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs Outdated
Comment thread VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)

118-122: Fix assertion path to include Images subdirectory.

Files are saved under RootPath/ImagesSubPath; the current assertion points to RootPath only, so it will falsely fail.

-        var filePath = Path.Combine(_tempDir, $"{_fileName}.png");
+        var filePath = Path.Combine(_tempDir, _subDir, $"{_fileName}.png");
♻️ Duplicate comments (4)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)

5-6: Make both properties init-only (aligns with prior feedback).

These are configuration values set at startup and never mutated at runtime. Using init enforces immutability and matches ImagesSubPath. This also addresses the earlier “Why aren't those init?” comment.

-    public required string RootPath { get; set; }
-    public required string ImagesSubPath { get; init; }
+    public required string RootPath { get; init; }
+    public required string ImagesSubPath { get; init; }
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (3)

10-11: Add XML docs listing thrown exceptions per method.

Prior feedback requested documenting exception types. Adding tags will help API consumers handle failures properly.

Example:

/// <summary>Saves an image to blob storage.</summary>
/// <exception cref="BlobFileNameException">Invalid blob name.</exception>
/// <exception cref="InvalidBase64FormatException">Base64 invalid.</exception>
/// <exception cref="BlobFileSystemException">I/O or filesystem issues.</exception>
public Task<string> SaveFileInStorageAsync(string base64, string name, string mimeType) { ... }

15-20: Guard directory creation and use FullPath consistently.

Directory creation can fail (permissions/disk). Wrap it and prefer _blobEnv.FullPath for consistency. This echoes the earlier suggestion.

-    public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor)
-    {
-        _blobEnv = environment.Value;
-        _httpContextAccessor = httpContextAccessor;
-        Directory.CreateDirectory(Path.Combine(_blobEnv.RootPath, _blobEnv.ImagesSubPath));
-    }
+    public BlobService(IOptions<BlobEnvironmentVariables> environment, IHttpContextAccessor httpContextAccessor)
+    {
+        _blobEnv = environment.Value;
+        _httpContextAccessor = httpContextAccessor;
+        try
+        {
+            Directory.CreateDirectory(_blobEnv.FullPath);
+        }
+        catch (Exception ex)
+        {
+            throw new BlobFileSystemException(_blobEnv.FullPath, "Failed to create blob storage directory.", ex);
+        }
+    }

74-80: Save-first, then delete to avoid data loss.

Current flow deletes the old blob before saving the new one. If the save fails, the asset is lost. Reverse the order.

     public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
     {
         ValidateFileName(newBlobName);
-        DeleteFileInStorage(previousBlobName, previousMimeType);
-        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        DeleteFileInStorage(previousBlobName, previousMimeType);
         return newBlobName;
     }

Optional: if you want transactional behavior, save to a temp name, then swap and delete on success.

🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1)

8-8: Normalize FullPath to avoid surprises with relative segments and mixed separators.

Path.GetFullPath() ensures the combined path is canonical (e.g., trims “.”/“..”, normalizes separators). It’s a low-risk hardening step.

-    public string FullPath => Path.Combine(RootPath, ImagesSubPath);
+    public string FullPath => Path.GetFullPath(Path.Combine(
+        RootPath,
+        ImagesSubPath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));

Follow-up (optional): If you want to guarantee ImagesSubPath does not escape RootPath, validate that FullPath starts with the normalized RootPath; otherwise throw during app startup.

VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4)

31-33: Optional: avoid redundant CreateDirectory at write-time.

You already ensure the directory exists in the constructor; this extra call is defensive but redundant. Either remove it, or keep it with a try-wrap for symmetry.


108-124: Preserve the inner exception when base64 decoding fails.

Re-throwing without the inner exception drops valuable diagnostics.

-        catch (Exception ex) when (ex is not InvalidBase64FormatException)
-        {
-            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64);
-        }
+        catch (Exception ex) when (ex is not InvalidBase64FormatException)
+        {
+            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex);
+        }

131-141: Expand/validate supported MIME types (or fail fast on unknown).

Defaulting to “jpg” can hide integration errors. Consider whitelisting and throwing for unsupported types or at least extending the map.

     private string GetExtensionFromMimeType(string mimeType)
     {
         return mimeType.ToLower() switch
         {
             "image/jpeg" => "jpg",
             "image/jpg" => "jpg",
             "image/png" => "png",
             "image/webp" => "webp",
-            _ => "jpg"
+            "image/gif" => "gif",
+            "image/svg+xml" => "svg",
+            "image/bmp" => "bmp",
+            "image/tiff" => "tiff",
+            _ => throw new ArgumentException(ImageConstants.MimeTypeValidationError(new[]
+            {
+                "image/jpeg","image/jpg","image/png","image/webp","image/gif","image/svg+xml","image/bmp","image/tiff"
+            }), nameof(mimeType))
         };
     }

If changing behavior is risky, keep the default but log a warning; add unit tests for each supported type.


10-20: Decouple BLL from HttpContext to improve testability and layering.

Injecting IHttpContextAccessor in BLL creates a Web dependency. Consider introducing an IUrlProvider abstraction or returning a relative path from BLL and composing the absolute URL in the Web layer.

VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (4)

71-87: Rename test to reflect URL semantics (not Base64).

The name still references Base64. Update for clarity.

-    public async Task GetFileUrl_ShouldReturnOriginalBase64()
+    public async Task GetFileUrl_ShouldBuildAbsoluteUrl()

45-48: Nit: “encryptedContent” is a leftover from previous implementation.

Rename to “savedContent” to avoid confusion.

-        var encryptedContent = File.ReadAllBytes(filePath);
+        var savedContent = File.ReadAllBytes(filePath);
-        var originalContent = Convert.FromBase64String(_base64);
-        Assert.Equal(originalContent, encryptedContent);
+        var originalContent = Convert.FromBase64String(_base64);
+        Assert.Equal(originalContent, savedContent);

89-101: Add coverage for PathBase and URL-encoding.

Given the updated GetFileUrl, add tests for:

  • PathBase = “/api/v1”
  • blob names with spaces or non-ASCII (“team logo 1”, “фото”)

I can draft these tests if you’d like.


103-113: Consider a regression test for update flow (save-first semantics).

Once UpdateFileInStorageAsync is refactored, add a test that forces the save to fail and ensures the old file remains intact.

Happy to provide a test that uses a mocked file system or injected file writer to simulate a write failure.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11b9492 and da6c467.

📒 Files selected for processing (5)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageToUrlResolver.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobEnvironmentVariables.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4 hunks)
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • VictoryCenter/VictoryCenter.BLL/Mapping/Images/ImageToUrlResolver.cs
  • VictoryCenter/VictoryCenter.IntegrationTests/ControllerTests/Images/GetByName/GetImageByNameTest.cs
🧰 Additional context used
🧬 Code graph analysis (2)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
  • BlobFileSystemException (3-18)
  • BlobFileSystemException (5-9)
  • BlobFileSystemException (11-15)
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
  • GetFileUrl (6-6)
  • DeleteFileInStorage (8-8)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
  • ImageConstants (3-40)
  • WrongFileName (36-39)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
  • ImageProcessingException (3-18)
  • ImageProcessingException (5-9)
  • ImageProcessingException (11-15)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
  • InvalidBase64FormatException (3-14)
  • InvalidBase64FormatException (5-8)
  • InvalidBase64FormatException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
  • BlobFileNameException (3-18)
  • BlobFileNameException (5-9)
  • BlobFileNameException (11-15)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
  • BlobService (10-189)
  • BlobService (15-20)
  • Task (22-40)
  • Task (42-48)
  • Task (74-80)
  • Task (143-157)
  • Task (159-178)
  • GetFileUrl (50-72)
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (1)
  • GetFileUrl (6-6)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
  • ImageConstants (3-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze
🔇 Additional comments (4)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)

143-157: Write-path exceptions mapped correctly. LGTM.

VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3)

131-141: Base64 failure path well-covered. LGTM.


152-165: Read failure path under lock is solid. LGTM.


167-178: Robust cleanup. LGTM.

Comment on lines +50 to 67
public string GetFileUrl(string name, string mimeType)
{
ValidateFileName(name);

try
{
using var stream = await FindFileInStorageAsMemoryStreamAsync(name, mimeType);
return Convert.ToBase64String(stream.ToArray());
var extension = GetExtensionFromMimeType(mimeType);
var fileName = $"{name}.{extension}";
HttpRequest? request = _httpContextAccessor.HttpContext?.Request;

if (request == null)
{
throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable);
}

var baseUrl = $"{request.Scheme}://{request.Host}";
return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden URL construction: respect PathBase, avoid double slashes, and URL-encode file names.

  • Include Request.PathBase for apps mounted under a sub-path.
  • Trim separators on ImagesSubPath to prevent “//”.
  • URL-encode file names to handle spaces and non-ASCII safely.
-    public string GetFileUrl(string name, string mimeType)
+    public string GetFileUrl(string name, string mimeType)
     {
         ValidateFileName(name);

         try
         {
             var extension = GetExtensionFromMimeType(mimeType);
-            var fileName = $"{name}.{extension}";
+            var fileName = $"{name}.{extension}";
+            var encodedFileName = Uri.EscapeDataString(fileName);
             HttpRequest? request = _httpContextAccessor.HttpContext?.Request;

             if (request == null)
             {
                 throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable);
             }

-            var baseUrl = $"{request.Scheme}://{request.Host}";
-            return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
+            var baseUrl = $"{request.Scheme}://{request.Host}";
+            var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty;
+            var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\');
+            return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}";
         }
         catch (Exception ex) when (ex is not BlobStorageException)
         {
-            throw new ImageProcessingException(_blobEnv.FullPath, ex.Message, ex);
+            throw new ImageProcessingException(fileName, ex.Message, ex);
         }
     }

Note: If you prefer framework help, inject LinkGenerator or use PathString to combine segments safely.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs around
lines 50 to 67, the URL construction ignores Request.PathBase, can produce
double slashes from ImagesSubPath, and doesn't URL-encode file names; update the
method to include request.PathBase (when present), trim leading/trailing slashes
from _blobEnv.ImagesSubPath, URL-encode the fileName (or the name before
appending extension), and build the final path by combining PathBase,
ImagesSubPath and fileName using safe join semantics
(PathString/Uri/LinkGenerator or manual trim + single '/' concatenation) so you
never emit "//" and non-ASCII/space characters are percent-encoded.

Comment thread VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)

36-39: Rename WrongFileName to reflect its semantics and align wording with FileNotFound

The method name implies an invalid filename, but the message describes a retrieval failure. Consider renaming to match behavior and keep phrasing consistent with FileNotFound.

Suggested change:

-    public static string WrongFileName(string name)
-    {
-        return $"An error occurred while retrieving the file {name}.";
-    }
+    public static string FailedToRetrieveFile(string fileName)
+    {
+        return $"Failed to retrieve file: {fileName}.";
+    }

Follow-up: update call sites accordingly. Using “fileName” also aligns parameter naming with FileNotFound’s “filePath”.

Find call sites to safely apply the rename:

#!/bin/bash
rg -nP -C2 '\bImageConstants\.WrongFileName\s*\('
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4)

15-27: Good catch: constructor now hardens directory creation with explicit error handling

Wrapping CreateDirectory in try/catch and surfacing BlobFileSystemException with a domain-specific message aligns with previous feedback and avoids silent failures.


10-11: Add XML docs for the service and public methods with declared exception contracts

Please add summary and exception tags for all public methods (e.g., SaveFileInStorageAsync, GetFileUrl, UpdateFileInStorageAsync, DeleteFileInStorage). This was requested earlier and will help consumers understand failure modes (BlobFileSystemException, ImageProcessingException, BlobNotFoundException, InvalidBase64FormatException, BlobFileNameException).


81-86: Avoid data loss in UpdateFileInStorageAsync: save new file before deleting the old one

Deleting first risks irrecoverable loss if the subsequent save fails. Swap the order; delete only after a successful save. Optionally, best-effort delete failures can be logged but should not fail the whole operation after a successful save.

     public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
     {
         ValidateFileName(newBlobName);
-        DeleteFileInStorage(previousBlobName, previousMimeType);
-        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
-        return newBlobName;
+        // 1) Save new first
+        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        // 2) Then delete old (best-effort)
+        try
+        {
+            DeleteFileInStorage(previousBlobName, previousMimeType);
+        }
+        catch (BlobStorageException)
+        {
+            // Consider logging; returning success keeps the new asset available
+            // Re-throw if your domain requires strict cleanup
+        }
+        return newBlobName;
     }

57-79: Harden URL generation: respect PathBase, URL-encode the file name, and fix exception context

  • Respect Request.PathBase for apps hosted under a sub-path.
  • Trim separators on ImagesSubPath to avoid double slashes.
  • URL-encode file names so spaces/non-ASCII are safe.
  • In the catch, pass the file identifier (not the storage directory) to ImageProcessingException.

This prevents broken links and clarifies error telemetry.

     public string GetFileUrl(string name, string mimeType)
     {
         ValidateFileName(name);

         try
         {
             var extension = GetExtensionFromMimeType(mimeType);
-            var fileName = $"{name}.{extension}";
+            var fileName = $"{name}.{extension}";
+            var encodedFileName = Uri.EscapeDataString(fileName);
             HttpRequest? request = _httpContextAccessor.HttpContext?.Request;

             if (request == null)
             {
                 throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.HttpContextIsNotAvailable);
             }

-            var baseUrl = $"{request.Scheme}://{request.Host}";
-            return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
+            var baseUrl = $"{request.Scheme}://{request.Host}";
+            var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty;
+            var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\');
+            return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}";
         }
         catch (Exception ex) when (ex is not BlobStorageException)
         {
-            throw new ImageProcessingException(_blobEnv.FullPath, ex.Message, ex);
+            // Use the logical file identifier rather than the directory path
+            var fileId = $"{name}.{GetExtensionFromMimeType(mimeType)}";
+            throw new ImageProcessingException(fileId, ex.Message, ex);
         }
     }
🧹 Nitpick comments (6)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)

16-19: Clarify and Standardize ImageConstants Naming

To improve clarity around the different “save image” errors and ensure consistent naming:

  • Distinguish scopes of save-image constants
    FailToSaveImageInStorage (used in BlobFileSystemException and unit tests) denotes failures writing to blob/file storage.
    FailedToSaveImage (used in ImageProcessingException in BlobService.cs) covers general image-processing save failures.
    If you’d like more explicit names, consider renaming to something like:
    FailedToSaveImageToStorage
    FailedToSaveImageAfterProcessing

  • Enforce the “FailedTo*” pattern for directory-creation errors
    Current constant breaks pattern:

- public static readonly string FailToCreateDirectory = "Failed to create blob storage directory";
+ public static readonly string FailedToCreateDirectory = "Failed to create blob storage directory";

Update all call sites in BlobService.cs (line 25) and any tests. To preserve backward compatibility, you could mark the old FailToCreateDirectory as [Obsolete] and introduce the new FailedToCreateDirectory in parallel for one release cycle.

Key call sites to update:

  • VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
    – Line 25: throw new BlobFileSystemException(..., ImageConstants.FailToCreateDirectory, ex);
  • VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
    – Line 162: throw new ImageProcessingException(..., ImageConstants.FailedToSaveImage, ex);
  • VictoryCenter.UnitTests/MediatRHandlersTests/Images/CreateImage.cs
    – Lines 169 & 184: references ImageConstants.FailToSaveImageInStorage
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5)

33-47: Normalize error mapping in SaveFileInStorageAsync to avoid leaking low-level exception messages

Right now unexpected exceptions bubble via ex.Message. Prefer a consistent, user-safe message (and keep the inner exception for diagnostics).

Apply:

-        catch (Exception ex) when (ex is not BlobStorageException)
-        {
-            throw new BlobFileSystemException(_blobEnv.FullPath, ex.Message, ex);
-        }
+        catch (Exception ex) when (ex is not BlobStorageException)
+        {
+            // Keep inner exception for logs; present a stable domain message outward
+            throw new BlobFileSystemException(_blobEnv.FullPath, ImageConstants.FailToSaveImageInStorage, ex);
+        }

49-55: Return a read-only MemoryStream to prevent accidental mutation

Creating the stream as non-writable communicates intent and avoids accidental writes in consumers.

-        return new MemoryStream(decodedBytes);
+        return new MemoryStream(decodedBytes, writable: false);

108-136: Preserve inner exception and avoid extra allocations when parsing Base64

  • Use Split with a max count to avoid unnecessary array allocations.
  • Preserve the inner exception when rethrowing to aid diagnostics (you already have an overload).
-        if (base64.Contains(','))
-        {
-            base64 = base64.Split(',')[1];
-        }
+        if (base64.Contains(','))
+        {
+            base64 = base64.Split(',', 2)[1];
+        }

         var byteCount = base64.Length * 3 / 4;
         var buffer = ArrayPool<byte>.Shared.Rent(byteCount);
         try
         {
             if (!Convert.TryFromBase64String(base64, buffer, out var bytesWritten))
             {
                 throw new InvalidBase64FormatException(ImageConstants.InvalidBase64String);
             }

             var result = new byte[bytesWritten];
             Array.Copy(buffer, result, bytesWritten);
             return result;
         }
-        catch (Exception ex) when (ex is not InvalidBase64FormatException)
-        {
-            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64);
-        }
+        catch (Exception ex) when (ex is not InvalidBase64FormatException)
+        {
+            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex);
+        }
         finally
         {
             ArrayPool<byte>.Shared.Return(buffer);
         }

187-195: Strengthen file name validation by explicitly rejecting path separators

On Linux, backslash is not an invalid file name char; explicitly reject both directory separators to be platform-agnostic and prevent path injection.

-    if (string.IsNullOrWhiteSpace(name)
-        || name.Contains("..")
-        || Path.GetInvalidFileNameChars().Any(name.Contains))
+    if (string.IsNullOrWhiteSpace(name)
+        || name.Contains("..")
+        || name.Contains(Path.DirectorySeparatorChar)
+        || name.Contains(Path.AltDirectorySeparatorChar)
+        || Path.GetInvalidFileNameChars().Any(c => name.Contains(c)))
     {
         throw new BlobFileNameException(name, ImageConstants.WrongFileName(name));
     }

57-74: Optional: Use LinkGenerator/PathString to construct URLs and support non-HTTP contexts

If HttpContext can be absent (background jobs, messaging handlers), consider:

  • Injecting LinkGenerator to build paths safely (handles PathBase).
  • Falling back to a configured BaseUrl when HttpContext is null.
    This keeps URL generation reliable beyond web-request pipelines.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between da6c467 and 8c3d48f.

📒 Files selected for processing (2)
  • VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
  • BlobFileSystemException (3-18)
  • BlobFileSystemException (5-9)
  • BlobFileSystemException (11-15)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
  • ImageConstants (3-40)
  • WrongFileName (36-39)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (3-14)
  • BlobStorageException (5-8)
  • BlobStorageException (10-13)
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
  • GetFileUrl (6-6)
  • DeleteFileInStorage (8-8)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
  • ImageProcessingException (3-18)
  • ImageProcessingException (5-9)
  • ImageProcessingException (11-15)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
  • InvalidBase64FormatException (3-14)
  • InvalidBase64FormatException (5-8)
  • InvalidBase64FormatException (10-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
  • BlobFileNameException (3-18)
  • BlobFileNameException (5-9)
  • BlobFileNameException (11-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze
🔇 Additional comments (3)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (3)

150-164: CreateFileAsync: solid error mapping and context

Validating the name, composing a deterministic path, and mapping failures to ImageProcessingException with the file identifier is spot on.


166-185: GetFileAsync: good use of BlobNotFoundException and contextual error mapping

Clear, minimal, and precise. Nice.


138-147: No action needed—extension derivation is consistent

I reviewed every call site of GetFileUrl and confirmed that it always receives the same mimeType value that was used when the blob was saved or updated.

  • Create/Update handlers persist the DTO’s MimeType into Image.MimeType and immediately use that exact value when saving the file (via GetExtensionFromMimeType) and later when resolving URLs.
  • Validators restrict MimeType to the allowed set (image/jpeg, image/jpg, image/png, image/webp), so the derived extension will always match one of the switch cases.
  • Unit and integration tests cover all supported MIME types—including mixed-case and “jpg” vs. “jpeg”—and assert the correct file extension is both written to disk and returned in the URL.

Since all saved blobs carry their own validated MIME string and every URL lookup derives its extension from that same string, there is no upstream mismatch risk in the current code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)

115-122: Fix: asserting deletion at the wrong path (false-positive risk)

The saved file lives under the Images subdir; this assertion checks the root temp dir instead and could pass even if deletion fails.

-        var filePath = Path.Combine(_tempDir, $"{_fileName}.png");
+        var filePath = Path.Combine(Path.Combine(_tempDir, _subDir), $"{_fileName}.png");
         Assert.False(File.Exists(filePath));
♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (1)

160-167: Avoid data loss when updating: save new first, delete old after successful save

Deleting the old blob before saving the new one risks permanent loss if the save fails mid-flight.

     public async Task<string> UpdateFileInStorageAsync(string previousBlobName, string previousMimeType, string base64Format, string newBlobName, string mimeType)
     {
         ValidateFileName(newBlobName);
         ValidateFileName(previousBlobName);
-        DeleteFileInStorage(previousBlobName, previousMimeType);
-        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        await SaveFileInStorageAsync(base64Format, newBlobName, mimeType);
+        DeleteFileInStorage(previousBlobName, previousMimeType);
         return newBlobName;
     }

Optionally log delete failures and continue to avoid blocking the update if cleanup fails.

🧹 Nitpick comments (9)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (2)

5-8: Doc comment punctuation/flow nit: collapse into one sentence

Currently renders as “…HttpContext/>. during…”. Merge the two lines for cleaner XML docs.

 /// <summary>
-/// Represents errors that occur when working with <see cref="HttpContext"/>.
-/// during blob storage operations (e.g., when building URLs).
+/// Represents errors that occur when working with <see cref="HttpContext"/> during blob storage operations (e.g., when building URLs).
 /// </summary>

9-20: Consider sealing and adding serialization constructor

Exception types are typically sealed and (optionally) serializable for completeness.

+using System.Runtime.Serialization;
@@
-public class BlobHttpContextException : BlobStorageException
+[Serializable]
+public sealed class BlobHttpContextException : BlobStorageException
 {
     public BlobHttpContextException(string message)
         : base(message)
     {
     }
 
     public BlobHttpContextException(string message, Exception innerException)
         : base(message, innerException)
     {
     }
+
+    protected BlobHttpContextException(SerializationInfo info, StreamingContext context)
+        : base(info.GetString(nameof(info)), new Exception()) // adapt if base adds serialization
+    {
+    }
 }

If your solution doesn’t use binary serialization, you can skip the constructor and just mark the class sealed.

VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (6)

76-91: XML docs: stray “/// ///”

There’s a duplicated XML doc prefix on Line 85 that hurts IDE rendering.

-    /// /// <exception cref="ImageProcessingException">
+    /// <exception cref="ImageProcessingException">

114-136: Harden URL construction: respect PathBase, trim slashes, and URL-encode file names

Without PathBase, apps hosted under a sub-path will generate broken links. Not encoding file names risks invalid URLs for spaces/non-ASCII. Trimming avoids “//”.

Please add tests covering PathBase and encoded names (see unit-test suggestion below).

     public string GetFileUrl(string name, string mimeType)
     {
         ValidateFileName(name);
 
         try
         {
             var extension = GetExtensionFromMimeType(mimeType);
-            var fileName = $"{name}.{extension}";
+            var fileName = $"{name}.{extension}";
+            var encodedFileName = Uri.EscapeDataString(fileName);
             HttpRequest? request = _httpContextAccessor.HttpContext?.Request;
 
             if (request == null)
             {
                 throw new BlobHttpContextException(ImageConstants.HttpContextIsNotAvailable);
             }
 
-            var baseUrl = $"{request.Scheme}://{request.Host}";
-            return $"{baseUrl}/{_blobEnv.ImagesSubPath}/{fileName}";
+            var baseUrl = $"{request.Scheme}://{request.Host}";
+            var pathBase = request.PathBase.HasValue ? request.PathBase.Value.TrimEnd('/') : string.Empty;
+            var imagesPath = _blobEnv.ImagesSubPath.Trim('/', '\\');
+            return $"{baseUrl}{pathBase}/{imagesPath}/{encodedFileName}";
         }
         catch (Exception ex) when (ex is not BlobStorageException)
         {
             throw new BlobFileSystemException(name, ex.Message, ex);
         }
     }

184-186: Remove unused variable and inline computation

fullName is only used once to compute filePath; inline to reduce noise.

-        var fullName = name + "." + GetExtensionFromMimeType(mimeType);
-        var filePath = Path.Combine(_blobEnv.FullPath, fullName);
+        var filePath = Path.Combine(_blobEnv.FullPath, $"{name}.{GetExtensionFromMimeType(mimeType)}");

219-222: Preserve inner exception for easier diagnostics

Re-throwing without inner exception hides the original cause.

-        catch (Exception ex) when (ex is not InvalidBase64FormatException)
-        {
-            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64);
-        }
+        catch (Exception ex) when (ex is not InvalidBase64FormatException)
+        {
+            throw new InvalidBase64FormatException(ImageConstants.FailedToConvertBase64, ex);
+        }

231-238: Broaden MIME type support (gif, svg, avif, heic)

Fallback to jpg may mislabel content and break consumers. Recognize common modern formats.

     private string GetExtensionFromMimeType(string mimeType)
     {
         return mimeType.ToLower() switch
         {
             "image/jpeg" => "jpg",
             "image/jpg" => "jpg",
             "image/png" => "png",
             "image/webp" => "webp",
+            "image/gif" => "gif",
+            "image/svg+xml" => "svg",
+            "image/avif" => "avif",
+            "image/heic" => "heic",
             _ => "jpg"
         };
     }

278-286: Reduce over-restriction: “..” check is unnecessary once path separators are blocked

Since path separators are disallowed, “..” cannot traverse and may be valid in legitimate names (e.g., “v1..backup”). Consider removing it.

-        if (string.IsNullOrWhiteSpace(name)
-            || name.Contains("..")
-            || Path.GetInvalidFileNameChars().Any(name.Contains))
+        if (string.IsNullOrWhiteSpace(name)
+            || Path.GetInvalidFileNameChars().Any(name.Contains))
         {
             throw new BlobFileNameException(name, ImageConstants.WrongFileName(name));
         }
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (1)

71-87: Rename for clarity and add PathBase assertion

Method name still says “Base64” though we now return URLs. Also, consider asserting PathBase handling once BlobService is updated.

-    public async Task GetFileUrl_ShouldReturnOriginalBase64()
+    public async Task GetFileUrl_ShouldReturnAbsoluteUrl()
@@
-        Assert.Equal("https://example.com/Images/image123.png", result);
+        Assert.Equal("https://example.com/Images/image123.png", result);

Additionally, add a test where request.PathBase = "/api" and ensure the URL is “https://example.com/api/Images/image123.png” after the BlobService change.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8c3d48f and 3b81e08.

📒 Files selected for processing (9)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs
🧰 Additional context used
🧬 Code graph analysis (3)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (6-17)
  • BlobStorageException (8-11)
  • BlobStorageException (13-16)
VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (7)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
  • BlobService (10-287)
  • BlobService (24-36)
  • Task (57-74)
  • Task (91-97)
  • Task (160-167)
  • Task (241-255)
  • Task (257-276)
  • GetFileUrl (114-136)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
  • BlobFileNameException (7-22)
  • BlobFileNameException (9-13)
  • BlobFileNameException (15-19)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (3)
  • BlobHttpContextException (9-20)
  • BlobHttpContextException (11-14)
  • BlobHttpContextException (16-19)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
  • InvalidBase64FormatException (7-18)
  • InvalidBase64FormatException (9-12)
  • InvalidBase64FormatException (14-17)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (1)
  • ImageConstants (3-40)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (2)
  • BlobNotFoundException (7-16)
  • BlobNotFoundException (9-13)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
  • ImageProcessingException (7-22)
  • ImageProcessingException (9-13)
  • ImageProcessingException (15-19)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (8)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (3)
  • BlobFileSystemException (7-22)
  • BlobFileSystemException (9-13)
  • BlobFileSystemException (15-19)
VictoryCenter/VictoryCenter.BLL/Constants/ImageConstants.cs (2)
  • ImageConstants (3-40)
  • WrongFileName (36-39)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (6-17)
  • BlobStorageException (8-11)
  • BlobStorageException (13-16)
VictoryCenter/VictoryCenter.BLL/Interfaces/BlobStorage/IBlobService.cs (2)
  • GetFileUrl (6-6)
  • DeleteFileInStorage (8-8)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (3)
  • BlobHttpContextException (9-20)
  • BlobHttpContextException (11-14)
  • BlobHttpContextException (16-19)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (3)
  • ImageProcessingException (7-22)
  • ImageProcessingException (9-13)
  • ImageProcessingException (15-19)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (3)
  • InvalidBase64FormatException (7-18)
  • InvalidBase64FormatException (9-12)
  • InvalidBase64FormatException (14-17)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (3)
  • BlobFileNameException (7-22)
  • BlobFileNameException (9-13)
  • BlobFileNameException (15-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze
🔇 Additional comments (10)
VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (2)

12-14: DI choices look good

Storing BlobEnvironmentVariables and IHttpContextAccessor as readonly fields is appropriate for this service’s responsibilities.


15-36: Constructor hardening is solid

Creating the directory in a guarded block and mapping failures to BlobFileSystemException with a meaningful path/message is exactly what we want here.

VictoryCenter/VictoryCenter.UnitTests/ServiceTests/BlobService.cs (8)

38-48: Test covers content integrity—nice

Verifies persisted bytes match decoded Base64 and asserts expected file path under subdir. Good guardrail for regressions.


50-60: Good negative-path coverage on invalid names

Asserting message contains the invalid name improves diagnosability of failures.


62-69: Stream retrieval test is clear and robust

Roundtrip check via MemoryStream validates read path behavior. LGTM.


89-101: Good guard: null HttpRequest → BlobHttpContextException

Ensures callers don’t silently get malformed URLs. Nice.


131-141: Invalid Base64 path covered well

Asserting specific message aligns with ImageConstants and helps catch regressions.


142-150: Not-found read path test looks good

Confirms BlobNotFoundException surfaces with informative message.


151-165: Robust “file locked” scenario

Great to see a realistic IO exception path validated with the expected ImageProcessingException wrapper.


167-179: Cleanup is resilient

Normalizing attributes before recursive delete avoids stubborn temp files on Windows. Nicely done.

namespace VictoryCenter.BLL.Exceptions.BlobStorageExceptions;

/// <summary>
/// Represents errors that occur when a requested file is not found.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about punctuation? It doesn't look like an end of a sentence.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)

3-7: Good domain-specific exception with clear XML summary — thanks for adding it

The class purpose is clear and aligns with the BlobStorageException hierarchy. This directly addresses the earlier request to document what each exception is responsible for.

🧹 Nitpick comments (5)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (5)

6-6: Seal the exception to prevent unintended inheritance

Exception types are rarely intended as base classes. Sealing avoids accidental subclassing and keeps the hierarchy simple.

-public class BlobFileNameException : BlobStorageException
+public sealed class BlobFileNameException : BlobStorageException

8-12: Add the file name to Exception.Data for easier telemetry correlation

Attaching the contextual key to Exception.Data helps centralized logging/telemetry without forcing string parsing of the message.

     public BlobFileNameException(string name, string message)
         : base(message)
     {
         Name = name;
+        Data["BlobFileName"] = name;
     }

     public BlobFileNameException(string name, string message, Exception innerException)
         : base(message, innerException)
     {
         Name = name;
+        Data["BlobFileName"] = name;
     }

If you apply the earlier rename, replace name with fileName.

Also applies to: 14-18


8-12: Document the constructors and property for IntelliSense completeness

Minor, but helpful for consumers. If you decide not to rename, keep the tags in sync with the current identifiers.

+    /// <summary>
+    /// Initializes a new instance of the <see cref="BlobFileNameException"/> class.
+    /// </summary>
+    /// <param name="name">The invalid blob file name associated with the error.</param>
+    /// <param name="message">The error message that explains the reason for the exception.</param>
     public BlobFileNameException(string name, string message)
         : base(message)
     {
         Name = name;
     }
 
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BlobFileNameException"/> class with an inner exception.
+    /// </summary>
+    /// <param name="name">The invalid blob file name associated with the error.</param>
+    /// <param name="message">The error message that explains the reason for the exception.</param>
+    /// <param name="innerException">The exception that is the cause of the current exception.</param>
     public BlobFileNameException(string name, string message, Exception innerException)
         : base(message, innerException)
     {
         Name = name;
     }
 
+    /// <summary>
+    /// Gets the blob file name related to this exception.
+    /// </summary>
     public string Name { get; }

Also applies to: 14-18, 20-20


8-18: Sanitize logged file names if they can contain full paths or PII

If exception messages or the Name/FileName value are logged, ensure they do not leak absolute paths or user-identifying segments. Prefer storing only the file name (no directory) or a hashed value if necessary.

Happy to help add a small helper (e.g., IPathRedactor.Redact) and use it at throw sites if needed.


8-12: Rename exception property and parameter for clarity

The BlobFileNameException API currently exposes a generic Name property and constructor parameter. Renaming these to FileName will improve readability at call sites and in logs, and align with any other blob-related exceptions that use FileName or BlobName. Since this is a public API change, consider providing a deprecated alias for one release cycle to avoid breaking existing consumers.

Locations to update:

  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs
    • Rename ctor parameters from string name to string fileName in both overloads
    • Change assignments from Name = name; to FileName = fileName;
    • Rename the auto-property from public string Name { get; } to public string FileName { get; }
    • (Optional but recommended) Add a [Obsolete("Use FileName instead")] public string Name => FileName; alias

Example diff in BlobFileNameException.cs:

-    public BlobFileNameException(string name, string message)
+    public BlobFileNameException(string fileName, string message)
         : base(message)
     {
-        Name = name;
+        FileName = fileName;
     }

-    public BlobFileNameException(string name, string message, Exception innerException)
+    public BlobFileNameException(string fileName, string message, Exception innerException)
         : base(message, innerException)
     {
-        Name = name;
+        FileName = fileName;
     }

-    public string Name { get; }
+    public string FileName { get; }
+    [Obsolete("Use FileName instead")]
+    public string Name => FileName;
  • No call-site changes are required in BlobService.cs, as the constructor invocation still passes the local name variable.
  • After renaming, run a quick search to confirm no other code or tests reference the old Name member.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b81e08 and 4926ba6.

📒 Files selected for processing (7)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/ImageProcessingException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobHttpContextException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/InvalidBase64FormatException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobNotFoundException.cs
  • VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileSystemException.cs
  • VictoryCenter/VictoryCenter.BLL/Services/BlobStorage/BlobService.cs
🧰 Additional context used
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobFileNameException.cs (1)
VictoryCenter/VictoryCenter.BLL/Exceptions/BlobStorageExceptions/BlobStorageException.cs (3)
  • BlobStorageException (6-17)
  • BlobStorageException (8-11)
  • BlobStorageException (13-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze

@VolodymyrSushynskyi VolodymyrSushynskyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs (1)

1-12: Ensure xUnit namespace is imported
Tests use the [Fact] attribute but there’s no using Xunit; (nor a global using); add using Xunit; at the top of VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs or declare it in a shared Usings.cs to resolve the attribute.

🧹 Nitpick comments (20)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/ProgramCategories/GetProgramCategoriesTests.cs (1)

87-89: Mocks updated to new URL-based contract — looks good.

Switch to GetFileUrl with a synchronous Returns is correct for the new interface.

To reduce duplication across tests, consider a shared test constant for the URL.

-            .Returns("https://localhost:5000/supersecretimage.png");
+            .Returns(TestImageData.ImageUrl);
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/CreateProgramTests.cs (1)

130-132: Correct API usage; but this setup appears unused in this test.

AutoMapper mapping is stubbed to return a prebuilt DTO, so the blob service isn’t exercised. Either remove the setup or assert it’s not called to keep the test intention clear.

Minimal options:

-        _blobServiceMock
-            .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()))
-            .Returns("https://localhost:5000/supersecretimage.png");
+        // Not needed: mapping is stubbed and does not invoke blob service in this test.

Or verify zero usage:

         Result<ProgramDto> result = await handler.Handle(new CreateProgramCommand(_createProgramDto), CancellationToken.None);
         Assert.True(result.IsSuccess);
         Assert.Equal(result.Value.Name, _programEntity.Name);
+        _blobServiceMock.VerifyNoOtherCalls();
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPublishedProgramsTests.cs (1)

96-98: Aligned with GetFileUrl — OK.

Same note as other tests: if mapping is stubbed, this setup likely doesn’t run. Consider removing or centralizing the URL constant.

VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs (2)

21-26: Constructor injects IBlobService but never uses it.

This introduces dead code and can confuse readers. Two paths:

  • Short-term (no breaking changes): explicitly discard the dependency to document intent.
  • Better (follow-up): remove the parameter and update DI/tests.

Apply the short-term fix:

 public CreateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<CreateProgramCommand> validator, IBlobService blobService)
 {
     _mapper = mapper;
     _repositoryWrapper = repositoryWrapper;
     _validator = validator;
+    _ = blobService; // intentionally unused; AutoMapper resolvers obtain it via DI
 }

70-73: Catching BlobStorageException is fine; consider narrowing scope.

If only the mapping step can throw blob-related errors, wrap just the mapping in the catch to avoid masking unrelated faults.

-            if (await _repositoryWrapper.SaveChangesAsync() > 0)
-            {
-                return Result.Ok(_mapper.Map<ProgramDto>(entity));
-            }
+            if (await _repositoryWrapper.SaveChangesAsync() > 0)
+            {
+                try
+                {
+                    return Result.Ok(_mapper.Map<ProgramDto>(entity));
+                }
+                catch (BlobStorageException)
+                {
+                    return Result.Fail<ProgramDto>(ProgramConstants.FailedRetrievingProgramPhoto);
+                }
+            }
-        }
-        catch (BlobStorageException)
-        {
-            return Result.Fail<ProgramDto>(ProgramConstants.FailedRetrievingProgramPhoto);
-        }
+        }
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPrograms.cs (1)

191-193: Good switch to GetFileUrl; minor test hygiene.

As with the other tests, if AutoMapper is stubbed, this setup doesn’t execute. Remove it or verify interactions. Also consider a shared TestImageUrl constant.

-        _blobService
-            .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()))
-            .Returns("https://localhost:5000/supersecretimage.png");
+        // Not required here: mapping is stubbed.
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs (4)

94-95: Good switch to URL-based blob retrieval; add stronger assertions and invocation verification

The mock aligns with the new IBlobService API. To actually exercise the new flow, assert the returned URL on the DTO and verify the blob service was called. Also, prefer a neutral domain in tests.

Apply within this hunk:

-            .Returns("https://localhost:5000/supersecretimage.png");
+            .Returns("https://example.com/image.png");

And add the following (outside this hunk) in the success test, after asserting Status and before the existing verifications:

+        Assert.NotNull(result.Value.Image);
+        Assert.Equal("https://example.com/image.png", result.Value.Image.Url);
+        _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Once);

45-60: Validate image URL on success and verify GetFileUrl is invoked once

Without checking Image.Url and the invocation count, we could regress without noticing.

Apply:

         Assert.Equal(_programDto.Status, result.Value.Status);
+        Assert.NotNull(result.Value.Image);
+        Assert.Equal("https://example.com/image.png", result.Value.Image.Url);

         _mockRepositoryWrapper.Verify(x => x.ProgramsRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<DAL.Entities.Program>>()), Times.Once);
-        _mapperMock.Verify(x => x.Map<ProgramDto>(It.IsAny<DAL.Entities.Program>()), Times.Once);
+        _mapperMock.Verify(x => x.Map<ProgramDto>(It.IsAny<DAL.Entities.Program>()), Times.Once);
+        _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Once);

62-71: Ensure blob service is not called on not-found path

This tightens the negative-path behavior for the new URL logic.

Apply:

         Assert.False(result.IsSuccess);
         Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(Program)), result.Errors[0].Message);
+        _mockBlobService.Verify(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()), Times.Never);

70-70: Avoid potential type ambiguity in error assertion

Be explicit about the entity type to future-proof the message against any other Program types in scope.

Apply:

-        Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(Program)), result.Errors[0].Message);
+        Assert.Equal(ErrorMessagesConstants.NotFound(_programEntity.Id, typeof(VictoryCenter.DAL.Entities.Program)), result.Errors[0].Message);
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (4)

262-264: Avoid reusing a disposed TransactionScope in mocks

Moq .Returns(value) reuses the same instance per call setup. Future refactors that call BeginTransaction() multiple times in one handler execution could hit a disposed scope. Prefer a factory delegate to return a fresh scope each time.

-        _mockRepositoryWrapper.Setup(x => x.BeginTransaction())
-            .Returns(new TransactionScope(TransactionScopeAsyncFlowOption.Enabled));
+        _mockRepositoryWrapper.Setup(x => x.BeginTransaction())
+            .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled));

82-106: Minor: InlineData values read like names but represent descriptions

Your theory data ("Valid Name", "Updated Name", "A") is used as Description. Consider renaming the strings to avoid confusion in future maintenance.

Example:

-    [InlineData("Valid Name")]
-    [InlineData("Updated Name")]
-    [InlineData("A")]
+    [InlineData("Valid description")]
+    [InlineData("Another description")]
+    [InlineData("A")]

151-156: Avoid mutating shared fixture fields in tests

Mutating _testUpdatedTeamMember.FullName inside a test can create subtle coupling. Prefer a local clone to keep tests self-contained.

-        _testUpdatedTeamMember.FullName = testName!;
+        var localTeamMember = _testUpdatedTeamMember with { FullName = testName! };
+        _mockMapper.Reset();
+        _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>()))
+                   .Returns(localTeamMember);

238-268: Add coverage for ImageId path and category change priority

Two critical branches in the handler aren’t exercised here:

  • When ImageId is provided, the handler fetches Image and maps it.
  • When CategoryId changes, Priority should become max+1.

Consider adding targeted tests for both.

Sample test sketch (add as separate facts):

[Fact]
public async Task Handle_WhenImageIdProvided_ShouldLoadImageAndMapUrl()
{
    var existing = _testExistingTeamMember;
    var toUpdate = _testUpdatedTeamMember with { ImageId = 42 };
    var image = new Image { Id = 42, BlobName = "img.png", MimeType = "image/png" };

    _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<TeamMember>>()))
        .ReturnsAsync(existing);
    _mockRepositoryWrapper.Setup(r => r.ImageRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<Image>>()))
        .ReturnsAsync(image);
    _mockRepositoryWrapper.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1);
    _mockRepositoryWrapper.Setup(r => r.BeginTransaction())
        .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled));

    _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>())).Returns(toUpdate);
    _mockMapper.Setup(m => m.Map<TeamMember, TeamMemberDto>(It.IsAny<TeamMember>()))
        .Returns(new TeamMemberDto { Id = toUpdate.Id, Image = new ImageDto { Url = "http://example/img.png" } });

    var handler = new UpdateTeamMemberHandler(_mockMapper.Object, _mockRepositoryWrapper.Object, _validator);
    var result = await handler.Handle(new UpdateTeamMemberCommand(new UpdateTeamMemberDto { FullName = "X", CategoryId = existing.CategoryId }, existing.Id), CancellationToken.None);

    Assert.True(result.IsSuccess);
    Assert.NotNull(result.Value.Image);
    Assert.False(string.IsNullOrWhiteSpace(result.Value.Image.Url));
}

[Fact]
public async Task Handle_WhenCategoryChanges_ShouldReassignPriorityToMaxPlusOne()
{
    var existing = _testExistingTeamMember;
    var toUpdate = _testUpdatedTeamMember with { CategoryId = 2 };

    _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<TeamMember>>()))
        .ReturnsAsync(existing);
    _mockRepositoryWrapper.Setup(r => r.TeamMembersRepository.MaxAsync(It.IsAny<Expression<Func<TeamMember, int?>>>(), It.IsAny<Expression<Func<TeamMember, bool>>>()))
        .ReturnsAsync(5);
    _mockRepositoryWrapper.Setup(r => r.CategoriesRepository.GetFirstOrDefaultAsync(It.IsAny<QueryOptions<Category>>()))
        .ReturnsAsync(new Category { Id = 2, Name = "New" });
    _mockRepositoryWrapper.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1);
    _mockRepositoryWrapper.Setup(r => r.BeginTransaction())
        .Returns(() => new TransactionScope(TransactionScopeAsyncFlowOption.Enabled));

    _mockMapper.Setup(m => m.Map<UpdateTeamMemberDto, TeamMember>(It.IsAny<UpdateTeamMemberDto>())).Returns(toUpdate);
    _mockMapper.Setup(m => m.Map<TeamMember, TeamMemberDto>(It.IsAny<TeamMember>()))
        .Returns(new TeamMemberDto { Id = toUpdate.Id, CategoryId = 2, Priority = 6 });

    var handler = new UpdateTeamMemberHandler(_mockMapper.Object, _mockRepositoryWrapper.Object, _validator);
    var result = await handler.Handle(new UpdateTeamMemberCommand(new UpdateTeamMemberDto { FullName = "X", CategoryId = 2 }, existing.Id), CancellationToken.None);

    Assert.True(result.IsSuccess);
    Assert.Equal(6, result.Value.Priority);
    Assert.Equal(2, result.Value.CategoryId);
}
VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs (2)

22-27: Remove unused IBlobService dependency from the constructor.

The handler no longer uses IBlobService directly. Keeping it increases coupling and complicates DI/tests.

Apply:

-    public UpdateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<UpdateProgramCommand> validator, IBlobService blobService)
+    public UpdateProgramHandler(IMapper mapper, IRepositoryWrapper repositoryWrapper, IValidator<UpdateProgramCommand> validator)
     {
         _mapper = mapper;
         _repositoryWrapper = repositoryWrapper;
         _validator = validator;
     }

90-93: Clarify why BlobStorageException is handled here.

No direct blob calls in this handler; the exception likely originates from mapping (e.g., URL resolvers). Add a brief comment to prevent future removal.

Apply:

+        // Mapping to ProgramDto may resolve image URLs via blob storage (e.g., AutoMapper resolver), so handle BlobStorageException.
         catch (BlobStorageException)
         {
             return Result.Fail<ProgramDto>(ProgramConstants.FailedToUpdateProgram);
         }
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/UpdateProgramTests.cs (4)

141-143: Remove unused blob service setup.

IMapper is fully mocked, and the handler doesn’t use IBlobService; this setup never executes.

Apply:

-    private void SetUpBlobService()
-    {
-        _blobServiceMock
-            .Setup(x => x.GetFileUrl(It.IsAny<string>(), It.IsAny<string>()))
-            .Returns("https://localhost:5000/supersecretimage.png");
-    }
+    // Blob service is not used in these unit tests because IMapper is mocked.

And stop calling it:

-        SetUpAutomapper();
-        SetUpBlobService();
-        SetUpRepositoryWrapper(saveResult, programEntity);
+        SetUpAutomapper();
+        SetUpRepositoryWrapper(saveResult, programEntity);

86-88: Verify repository interactions on success.

Assert that Update and SaveChanges were called once.

Apply:

         Assert.True(result.IsSuccess);
         Assert.Equal(result.Value.Name, _updateProgramDto.Name);
+        _repositoryWrapperMock.Verify(r => r.ProgramsRepository.Update(It.IsAny<DAL.Entities.Program>()), Times.Once);
+        _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Once);

100-102: Guard against side effects on validation failure.

Ensure no save attempt was made when validation fails.

Apply:

         Assert.False(result.IsSuccess);
         Assert.Contains("Validation failed", result.Errors[0].Message);
+        _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Never);

117-122: Strengthen NotFound test with interaction checks.

Confirm Update wasn’t called when entity is missing.

Apply:

         Assert.False(result.IsSuccess);
         Assert.Equal(ErrorMessagesConstants.NotFound(1, typeof(DAL.Entities.Program)), result.Errors[0].Message);
+        _repositoryWrapperMock.Verify(r => r.ProgramsRepository.Update(It.IsAny<DAL.Entities.Program>()), Times.Never);
+        _repositoryWrapperMock.Verify(r => r.SaveChangesAsync(), Times.Never);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4926ba6 and 875ec23.

📒 Files selected for processing (15)
  • VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs (1 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/ProgramCategories/GetProgramCategoriesHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetByFilters/GetProgramsByFiltersHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetById/GetProgramByIdHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetPublished/GetPublishedProgramsHandler.cs (0 hunks)
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs (3 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/ProgramCategories/GetProgramCategoriesTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/CreateProgramTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetProgramByIdTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPrograms.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/GetPublishedProgramsTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/Programs/UpdateProgramTests.cs (1 hunks)
  • VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (5 hunks)
  • VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs (1 hunks)
💤 Files with no reviewable changes (4)
  • VictoryCenter/VictoryCenter.BLL/Queries/ProgramCategories/GetProgramCategoriesHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetByFilters/GetProgramsByFiltersHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetById/GetProgramByIdHandler.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/Programs/GetPublished/GetPublishedProgramsHandler.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • VictoryCenter/VictoryCenter.WebAPI/Extensions/ServicesConfiguration.cs
  • VictoryCenter/VictoryCenter.BLL/Queries/TeamMembers/GetById/GetTeamMemberByIdHandler.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-20T18:22:51.823Z
Learnt from: Oleh-Bashtovyi
PR: ita-social-projects/VictoryCenter-Back#179
File: VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Reorder/ReorderTeamMembersHandler.cs:27-28
Timestamp: 2025-06-20T18:22:51.823Z
Learning: In the VictoryCenter codebase, FluentValidation is used for input validation in MediatR handlers. The handlers call ValidateAndThrowAsync() early in the Handle method, and validation exceptions are caught and converted to Result.Fail responses. This means validation logic should be kept in the validator classes rather than duplicated in the handlers.

Applied to files:

  • VictoryCenter/VictoryCenter.BLL/Commands/Programs/Create/CreateProgramHandler.cs
🧬 Code graph analysis (1)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (1)
VictoryCenter/VictoryCenter.BLL/Commands/TeamMembers/Update/UpdateTeamMemberHandler.cs (2)
  • UpdateTeamMemberHandler (15-107)
  • UpdateTeamMemberHandler (21-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and analyze
  • GitHub Check: Build and analyze
🔇 Additional comments (8)
VictoryCenter/VictoryCenter.UnitTests/MediatRHandlersTests/TeamMembers/UpdateTeamMemberTests.cs (5)

157-157: Constructor update repeated — looks good

This aligns with the refactor and keeps tests consistent.


184-184: Constructor update repeated — looks good

Consistent with earlier changes.


205-205: Constructor update repeated — looks good

No issues spotted.


223-223: Constructor update repeated — looks good

All handler constructions in this file now use the new signature.


124-124: Constructor update verified — LGTM
Instantiation matches the new 3-arg handler signature; no lingering 4-arg usages or IBlobService references remain in UpdateTeamMemberTests.

VictoryCenter/VictoryCenter.BLL/Commands/Programs/Update/UpdateProgramHandler.cs (3)

12-12: Correct namespace import.

Switching to the new BlobStorageExceptions namespace is consistent with the refactor.


58-67: Align ImageId null-handling and navigation property consistency.

If the DTO sets ImageId to null, the navigation property remains unchanged. Also, fetch is conditioned on programToUpdate.ImageId whereas the lookup uses request.updateProgramDto.ImageId. Prefer checking the DTO and explicitly clearing the nav prop; if a non-null ImageId is provided but not found, fail early.

Apply:

-            if (programToUpdate.ImageId != null)
+            if (request.updateProgramDto.ImageId.HasValue)
             {
                 Image? newImage = await _repositoryWrapper.ImageRepository.GetFirstOrDefaultAsync(new QueryOptions<Image>
                 {
                     Filter = image => image.Id == request.updateProgramDto.ImageId,
                     AsNoTracking = false
                 });
 
-                programToUpdate.Image = newImage;
+                if (newImage is null)
+                {
+                    return Result.Fail<ProgramDto>(ErrorMessagesConstants.NotFound(request.updateProgramDto.ImageId!.Value, typeof(Image)));
+                }
+                programToUpdate.Image = newImage;
             }
+            else
+            {
+                // DTO explicitly clears the image
+                programToUpdate.Image = null;
+            }

49-55: Validate category IDs exist to avoid partial updates.

Today, missing IDs silently drop from the assignment. Consider verifying the requested IDs against fetched IDs and failing fast if any are missing.

If helpful, I can propose a small helper to compute and report missing IDs.

Also applies to: 71-74

@taraskibysh
taraskibysh merged commit 45a7195 into release/1.0.0 Sep 2, 2025
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants