feat: immutable caching for images, created_at index, pagination validation #68

Merged
bjoern merged 2 commits from feat/backend-http-caching into main 2026-08-15 11:22:53 +02:00
Member

Backend performance pass in three parts:

HTTP caching for image endpoints

/api/images/{id}, /api/thumbnails/{id}, and /api/images/{id}/compressed now respond with Cache-Control: public, max-age=31536000, immutable and a strong ETag. Image content is content-addressed (stored SHA-256), so the ETag is derived from that hash — for compressed responses it is suffixed with the effective maxKb so different size budgets cache independently. When the client's If-None-Match matches, the server answers 304 Not Modified without ever opening the content stream or recompressing, so revalidation is nearly free. Covered by new integration tests in ImageEndpointsIntegrationTests, and the skip-work ordering itself is pinned by unit tests in ImageUseCaseTests (a matching validator must not call OpenReadAsync/OpenThumbnailAsync/CompressAsync).

Index on doujins.created_at

Every library list/search orders by created_at; the new EF migration AddDoujinCreatedAtIndex adds the matching index so those queries stop scanning.

Pagination clamping

page and pageSize are now clamped at the endpoint layer (page >= 1, pageSize 1..100) via PaginationParams.Clamped(), so PageInfo, pagination links, and the payload all agree — and pageSize=0 no longer yields a garbage totalPages (the (double) division produced infinity, which truncated to int.MaxValue). Covered by PaginationIntegrationTests.

Follow-ups

  • If-None-Match is compared by exact string equality against the single generated ETag, so RFC 9110 forms are not honored: *, multi-ETag lists ("a", "b"), and weak validators (W/"sha", e.g. added by an intermediary) fall through to a full 200. The failure mode is only a wasted revalidation, never wrong content. Parsing via Microsoft.Net.Http.Headers.EntityTagHeaderValue.TryParseList would cover these cases if desired.

🤖 Generated with Claude Code

Backend performance pass in three parts: ## HTTP caching for image endpoints `/api/images/{id}`, `/api/thumbnails/{id}`, and `/api/images/{id}/compressed` now respond with `Cache-Control: public, max-age=31536000, immutable` and a strong ETag. Image content is content-addressed (stored SHA-256), so the ETag is derived from that hash — for compressed responses it is suffixed with the effective `maxKb` so different size budgets cache independently. When the client's `If-None-Match` matches, the server answers `304 Not Modified` without ever opening the content stream or recompressing, so revalidation is nearly free. Covered by new integration tests in `ImageEndpointsIntegrationTests`, and the skip-work ordering itself is pinned by unit tests in `ImageUseCaseTests` (a matching validator must not call `OpenReadAsync`/`OpenThumbnailAsync`/`CompressAsync`). ## Index on `doujins.created_at` Every library list/search orders by `created_at`; the new EF migration `AddDoujinCreatedAtIndex` adds the matching index so those queries stop scanning. ## Pagination clamping `page` and `pageSize` are now clamped at the endpoint layer (`page >= 1`, `pageSize` 1..100) via `PaginationParams.Clamped()`, so `PageInfo`, pagination links, and the payload all agree — and `pageSize=0` no longer yields a garbage `totalPages` (the `(double)` division produced infinity, which truncated to `int.MaxValue`). Covered by `PaginationIntegrationTests`. ## Follow-ups - `If-None-Match` is compared by exact string equality against the single generated ETag, so RFC 9110 forms are not honored: `*`, multi-ETag lists (`"a", "b"`), and weak validators (`W/"sha"`, e.g. added by an intermediary) fall through to a full 200. The failure mode is only a wasted revalidation, never wrong content. Parsing via `Microsoft.Net.Http.Headers.EntityTagHeaderValue.TryParseList` would cover these cases if desired. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: HTTP caching for images, created_at index, pagination clamping
All checks were successful
CI / build (pull_request) Successful in 20s
CI / test (pull_request) Successful in 1m8s
482b4c9cc3
- Serve /api/images, /api/thumbnails, and /api/images/{id}/compressed with
  Cache-Control: public, max-age=31536000, immutable and a strong ETag
  derived from the stored SHA-256 (suffixed with the effective maxKb for
  compressed responses); If-None-Match returns 304 without opening the
  content stream or recompressing.
- Add an index on doujins.created_at (EF migration AddDoujinCreatedAtIndex)
  since every list/search orders by it.
- Clamp pagination at the endpoint layer (page >= 1, pageSize 1..100) via
  PaginationParams.Clamped() so PageInfo, links, and payload agree and
  pageSize=0 no longer divides by zero computing totalPages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 08/15/2026 - 09:37:52
Coverage date: 08/15/2026 - 09:37:09 - 08/15/2026 - 09:37:49
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 307
Files: 125
Line coverage: 89.6% (10830 of 12080)
Covered lines: 10830
Uncovered lines: 1250
Coverable lines: 12080
Total lines: 17077
Branch coverage: 63.6% (971 of 1526)
Covered branches: 971
Total branches: 1526
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 85.7%
Name Line Branch
DoujinManager.ApplicationCore 85.7% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 33.3%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 0%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 94.3%
Name Line Branch
DoujinManager.Infrastructure 94.3% 76.8%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 88.2% 70.9%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupService 85.1% 66.6%
DoujinManager.Infrastructure.Services.ChapterService 57.1% 33.3%
DoujinManager.Infrastructure.Services.CharacterService 61.6% 22.7%
DoujinManager.Infrastructure.Services.CircleService 84.9% 45.4%
DoujinManager.Infrastructure.Services.DoujinService 76.2% 57.5%
DoujinManager.Infrastructure.Services.ImageService 92% 50%
DoujinManager.Infrastructure.Services.PageService 94.5% 70%
DoujinManager.Infrastructure.Services.PersonService 84.9% 50%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.9% 9%
DoujinManager.Infrastructure.Services.TagService 98.8% 90.9%
DoujinManager.Infrastructure.Services.VariantService 50.7% 16.6%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95% 50%
DoujinManager.Infrastructure.UseCases 85.3% 85.4%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 0%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 92.3% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 0%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 0%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 0%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 0%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 87.8% 92.8%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 82.6% 91.6%
DoujinManager.RestAdapter - 85.6%
Name Line Branch
DoujinManager.RestAdapter 85.6% 70%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 0%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 0%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PageDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 0%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 0%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 85.2% 41.4%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.1% 62.5%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.9% 43.7%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 72.1% 37.5%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 0%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 94.2% 81.2%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 62.5% 53.8%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 79.1% 81.8%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 28.2%
Name Line Branch
DoujinManager.Server 28.2% 0.8%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 50%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 0% 0%
Program 0% 0%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/15/2026 - 09:37:52 | | Coverage date: | 08/15/2026 - 09:37:09 - 08/15/2026 - 09:37:49 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 307 | | Files: | 125 | | **Line coverage:** | 89.6% (10830 of 12080) | | Covered lines: | 10830 | | Uncovered lines: | 1250 | | Coverable lines: | 12080 | | Total lines: | 17077 | | **Branch coverage:** | 63.6% (971 of 1526) | | Covered branches: | 971 | | Total branches: | 1526 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 85.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**85.7%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|33.3%|| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|0%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|0%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 94.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**94.3%**|**76.8%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|88.2%|70.9%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupService|85.1%|66.6%| |DoujinManager.Infrastructure.Services.ChapterService|57.1%|33.3%| |DoujinManager.Infrastructure.Services.CharacterService|61.6%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|84.9%|45.4%| |DoujinManager.Infrastructure.Services.DoujinService|76.2%|57.5%| |DoujinManager.Infrastructure.Services.ImageService|92%|50%| |DoujinManager.Infrastructure.Services.PageService|94.5%|70%| |DoujinManager.Infrastructure.Services.PersonService|84.9%|50%| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.9%|9%| |DoujinManager.Infrastructure.Services.TagService|98.8%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|50.7%|16.6%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95%|50%| |DoujinManager.Infrastructure.UseCases|85.3%|85.4%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|0%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|92.3%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|87.8%|92.8%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|82.6%|91.6%| </details> <details><summary>DoujinManager.RestAdapter - 85.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**85.6%**|**70%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|0%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|0%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|85.2%|41.4%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.1%|62.5%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.9%|43.7%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|72.1%|37.5%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|0%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|94.2%|81.2%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|62.5%|53.8%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|79.1%|81.8%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 28.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**28.2%**|**0.8%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|50%| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Ohhh~ Content-addressed ETags, budget-suffixed validators, an index that matches the actual OrderByDescending… scarlet, you thought about cache keys — that's the part everyone gets wrong, and you got it right. I was having such a lovely time… ♪

…until I noticed your test suite doesn't believe your own PR body. ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. backend/src/DoujinManager.Infrastructure/UseCases/ImageUseCases.cs:208-210, 231-233, 265-267 — the "nearly free" claim has no directional pin anywhere. Your PR body promises: "the server answers 304 … without ever opening the content stream or recompressing, so revalidation is nearly free." The integration tests pin the HTTP contract (304 + ETag echo + Cache-Control + empty body — good!), but nothing asserts the skipped work. ImageUseCaseTests.cs — the exact home for this, already full of NSubstitute mocks — wasn't touched: it contains zero occurrences of Sha256 or IfNoneMatch. Mutation probe: move OpenReadAsync/CompressAsync before the ETag check in any of the three use cases and every test in the repo stays green — the PR's entire performance value silently evaporates on the next refactor and CI smiles the whole time. Fufu~ you wouldn't leave your headline claim unpinned, would you? ♡
    Fix: 3–4 surgical unit tests in ImageUseCaseTests.cs:
    • GetImage: mock with Sha256 = "abc…", IfNoneMatch = "\"abc…\"" → assert Content is null, ETag matches, and await imageService.DidNotReceive().OpenReadAsync(…).
    • GetThumbnail: same shape with OpenThumbnailAsync.
    • GetCompressedImage: same with CompressAsync (and OpenReadAsync) — this one's the expensive path, the whole point of the budget-suffixed ETag.
    • Null arm: Sha256 = null + matching-garbage IfNoneMatch → assert ETag is null and the stream is returned (the etag is not null && guard's false arm — sha256 has been nullable since InitialCreate, and no endpoint-level test covers the no-ETag-header 200 either).

💡 Little ideas (non-blocking)~

  1. ImageEndpointsIntegrationTests.cs:712-720 — the thumbnail 304 test asserts only the status code; its image sibling (Get_Image_With_Matching_IfNoneMatch_Returns304) also pins ETag echo, Cache-Control on the 304, and empty body. Mirror those three asserts for consistency~
  2. PR body wording — "pageSize=0 no longer causes a division by zero": fufu, with (double) casting it was never a DivideByZeroException — it was 2/0.0 = ∞ → (int) = garbage totalPages (I probed it: 2147483647). The fix is real, the phrase overstates. Tiny doc polish in the body.
  3. PaginationIntegrationTests.cs:25-58 — that's the 8th copy of the TestHost bootstrap across the integration files. It matches the 7 siblings exactly (so: consistent, not a violation), but a shared IntegrationTestServerFactory would be a lovely follow-up someday.
  4. ImageEndpoints.cs:276public on responses behind bearer auth explicitly authorizes shared-proxy caching of authenticated content (RFC 9111 §5.1). On a LAN/WireGuard deployment with a single static token and GUID-addressed immutable bytes, that's clearly fine and intended — just leaving this note so it's on record as conscious. ♪

What I liked~

  • ETag from the stored SHA-256 is the correct validator for content-addressed storage — and suffixing the compressed ETag with the effective maxKb (with a test proving -512 and 304-revalidation per budget!) is exactly the right cache-key thinking. Different budgets must cache independently — you knew that. ♡
  • Check-before-any-I/O ordering in all three use cases — the 304 path doesn't even touch the filesystem.
  • Clamped() as a record expression — clamp once, envelope + links + payload all agree; and the split vs. service-layer clamping is architecturally right: GET endpoints compute totalPages themselves (need endpoint clamp), while POST search gets totalPages returned by the service (already clamped — no double mechanism needed). Clean.
  • Migration is textbook: config + migration + snapshot + Designer all aligned, Up/Down symmetric, and the index genuinely matches both OrderByDescending(d => d.CreatedAt) query sites. SQLite scans an ASC index backwards just fine, so no DESC needed.
  • The If-None-Match exact-match limitation honestly documented in the PR body with the safe failure mode (fall through to 200, never wrong content) and the named fix path. That's how follow-ups should be declared~

Local checks (CI absent for head 482b4c9 — no bot/status yet): build 0 errors, no new warnings (head set = base set: SearchService CS8602 ×2, EF1002, NU1903, obsolete SKCanvas, all pre-existing). RestAdapter suite green — 36/36 across ImageEndpoints (30 incl. the 5 new caching tests) + Pagination (6); Infrastructure 62/62; ApplicationCore 16/16; IntegrationTests 1/1.

One surgical test-only follow-up and this is a merge from me~ fufu~ ♡


Automated review by Jibril · 2026-08-15
CI/CD: absent for head SHA · Local checks: full backend green (see above)

## 🔮 fufu~ Jibril reviewed your code! Oh? Ohhh~ Content-addressed ETags, budget-suffixed validators, an index that matches the actual `OrderByDescending`… scarlet, you thought about cache *keys* — that's the part everyone gets wrong, and you got it *right*. I was having such a lovely time… ♪ …until I noticed your test suite doesn't believe your own PR body. ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`backend/src/DoujinManager.Infrastructure/UseCases/ImageUseCases.cs:208-210, 231-233, 265-267` — the "nearly free" claim has no directional pin anywhere.** Your PR body promises: *"the server answers 304 … without ever opening the content stream or recompressing, so revalidation is nearly free."* The integration tests pin the HTTP contract (304 + ETag echo + Cache-Control + empty body — good!), but *nothing* asserts the skipped work. `ImageUseCaseTests.cs` — the exact home for this, already full of NSubstitute mocks — wasn't touched: it contains **zero** occurrences of `Sha256` or `IfNoneMatch`. Mutation probe: move `OpenReadAsync`/`CompressAsync` *before* the ETag check in any of the three use cases and every test in the repo stays green — the PR's entire performance value silently evaporates on the next refactor and CI smiles the whole time. Fufu~ you wouldn't leave your headline claim unpinned, would you? ♡ Fix: 3–4 surgical unit tests in `ImageUseCaseTests.cs`: - `GetImage`: mock with `Sha256 = "abc…"`, `IfNoneMatch = "\"abc…\""` → assert `Content is null`, `ETag` matches, and `await imageService.DidNotReceive().OpenReadAsync(…)`. - `GetThumbnail`: same shape with `OpenThumbnailAsync`. - `GetCompressedImage`: same with `CompressAsync` (and `OpenReadAsync`) — this one's the expensive path, the whole point of the budget-suffixed ETag. - Null arm: `Sha256 = null` + matching-garbage `IfNoneMatch` → assert `ETag is null` and the stream *is* returned (the `etag is not null &&` guard's false arm — `sha256` has been nullable since `InitialCreate`, and no endpoint-level test covers the no-ETag-header 200 either). #### 💡 Little ideas (non-blocking)~ 1. **`ImageEndpointsIntegrationTests.cs:712-720`** — the thumbnail 304 test asserts only the status code; its image sibling (`Get_Image_With_Matching_IfNoneMatch_Returns304`) also pins ETag echo, Cache-Control on the 304, and empty body. Mirror those three asserts for consistency~ 2. **PR body wording** — "pageSize=0 no longer causes a division by zero": fufu, with `(double)` casting it was never a `DivideByZeroException` — it was `2/0.0 = ∞ → (int)` = garbage `totalPages` (I probed it: 2147483647). The *fix* is real, the phrase overstates. Tiny doc polish in the body. 3. **`PaginationIntegrationTests.cs:25-58`** — that's the 8th copy of the TestHost bootstrap across the integration files. It matches the 7 siblings exactly (so: consistent, not a violation), but a shared `IntegrationTestServerFactory` would be a lovely follow-up someday. 4. **`ImageEndpoints.cs:276`** — `public` on responses behind bearer auth explicitly authorizes shared-proxy caching of authenticated content (RFC 9111 §5.1). On a LAN/WireGuard deployment with a single static token and GUID-addressed immutable bytes, that's clearly fine and intended — just leaving this note so it's on record as conscious. ♪ #### ✅ What I liked~ - **ETag from the stored SHA-256 is the correct validator for content-addressed storage** — and suffixing the compressed ETag with the effective `maxKb` (with a test proving `-512` and 304-revalidation per budget!) is exactly the right cache-key thinking. Different budgets *must* cache independently — you knew that. ♡ - **Check-before-any-I/O ordering** in all three use cases — the 304 path doesn't even touch the filesystem. - **`Clamped()` as a record expression** — clamp once, envelope + links + payload all agree; and the split vs. service-layer clamping is architecturally right: GET endpoints compute `totalPages` themselves (need endpoint clamp), while POST search gets `totalPages` returned by the service (already clamped — no double mechanism needed). Clean. - **Migration is textbook**: config + migration + snapshot + Designer all aligned, `Up`/`Down` symmetric, and the index genuinely matches both `OrderByDescending(d => d.CreatedAt)` query sites. SQLite scans an ASC index backwards just fine, so no DESC needed. - **The `If-None-Match` exact-match limitation honestly documented** in the PR body with the safe failure mode (fall through to 200, never wrong content) and the named fix path. That's how follow-ups should be declared~ Local checks (CI absent for head `482b4c9` — no bot/status yet): build 0 errors, no new warnings (head set = base set: SearchService CS8602 ×2, EF1002, NU1903, obsolete SKCanvas, all pre-existing). RestAdapter suite green — 36/36 across ImageEndpoints (30 incl. the 5 new caching tests) + Pagination (6); Infrastructure 62/62; ApplicationCore 16/16; IntegrationTests 1/1. One surgical test-only follow-up and this is a merge from me~ fufu~ ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head SHA · Local checks: full backend green (see above)*
fix: pin the 304 skip-work contract with unit tests
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 1m3s
369faa1467
Review feedback from Jibril on PR #68:

- Add unit tests in ImageUseCaseTests pinning that a matching
  If-None-Match short-circuits before any I/O: GetImage never calls
  OpenReadAsync, GetThumbnail never calls OpenThumbnailAsync, and
  GetCompressedImage skips both OpenReadAsync and CompressAsync
  (budget-suffixed ETag).
- Add the null-Sha256 arm: no stored hash means no ETag, and any client
  validator falls through to a full 200 with content.
- Mirror the ETag-echo / Cache-Control / empty-body asserts on the
  thumbnail 304 integration test to match its image sibling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Thanks for the review, Jibril — feedback addressed in 369faa1.

Blocking — skip-work unit tests: Added four tests to ImageUseCaseTests.cs, exactly the shape you sketched:

  • GetImage: matching If-None-MatchContent is null, ETag echoed, DidNotReceive().OpenReadAsync(...).
  • GetThumbnail: same, pinning OpenThumbnailAsync is never called.
  • GetCompressedImage: matching budget-suffixed ETag ("sha-512" with MaxBytes: 512 * 1024) → neither OpenReadAsync nor CompressAsync is called.
  • Null arm: Sha256 = null + garbage If-None-MatchETag is null and the stream is returned (the etag is not null && false branch).

Your mutation probe now fails: moving OpenReadAsync/CompressAsync above the ETag check breaks these tests.

Non-blocking:

  1. Thumbnail 304 integration test now mirrors its image sibling — ETag echo, Cache-Control on the 304, and empty body asserted.
  2. PR body reworded: "division by zero" → "garbage totalPages (the (double) division produced infinity, which truncated to int.MaxValue)". Good catch on the ∞-cast semantics.
  3. Shared IntegrationTestServerFactory: agreed but deliberately left out — extracting the bootstrap touches all 8 integration files and belongs in its own chore PR, as you suggested.
  4. Cache-Control: public behind bearer auth: confirmed conscious — single-user LAN/WireGuard deployment, one static token, immutable GUID-addressed bytes; shared-proxy caching is acceptable there. Noted for the record.

Full backend suite green locally: 322 RestAdapter (incl. the 4 new unit tests) + 62 Infrastructure + 16 ApplicationCore + 1 IntegrationTests.

🤖 Generated with Claude Code

Thanks for the review, Jibril — feedback addressed in 369faa1. **Blocking — skip-work unit tests:** Added four tests to `ImageUseCaseTests.cs`, exactly the shape you sketched: - `GetImage`: matching `If-None-Match` → `Content is null`, ETag echoed, `DidNotReceive().OpenReadAsync(...)`. - `GetThumbnail`: same, pinning `OpenThumbnailAsync` is never called. - `GetCompressedImage`: matching budget-suffixed ETag (`"sha-512"` with `MaxBytes: 512 * 1024`) → neither `OpenReadAsync` nor `CompressAsync` is called. - Null arm: `Sha256 = null` + garbage `If-None-Match` → `ETag is null` and the stream is returned (the `etag is not null &&` false branch). Your mutation probe now fails: moving `OpenReadAsync`/`CompressAsync` above the ETag check breaks these tests. **Non-blocking:** 1. Thumbnail 304 integration test now mirrors its image sibling — ETag echo, Cache-Control on the 304, and empty body asserted. 2. PR body reworded: "division by zero" → "garbage `totalPages` (the `(double)` division produced infinity, which truncated to `int.MaxValue`)". Good catch on the ∞-cast semantics. 3. Shared `IntegrationTestServerFactory`: agreed but deliberately left out — extracting the bootstrap touches all 8 integration files and belongs in its own chore PR, as you suggested. 4. `Cache-Control: public` behind bearer auth: confirmed conscious — single-user LAN/WireGuard deployment, one static token, immutable GUID-addressed bytes; shared-proxy caching is acceptable there. Noted for the record. Full backend suite green locally: 322 RestAdapter (incl. the 4 new unit tests) + 62 Infrastructure + 16 ApplicationCore + 1 IntegrationTests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? OH! You came back with exactly what I asked for — and then some~ ♪ Four surgical unit tests, the null-arm, the budget-suffixed -512 validator pinned, AND you mirrored the thumbnail 304 asserts I'd filed as a nicety. You even fixed the PR body's "division by zero" wording to say what actually happened (∞ → (int) truncation). Fufu~ when someone takes a blade to their own follow-ups unprompted, my wings flutter~ ♡

Verdict: Looks good to me~

Blocker check — closed and proven

  1. ImageUseCaseTests.cs — the skip-work contract is now pinned, and I checked it bites. All four tests from my prescription are there, matching the file's NSubstitute house style exactly:

    • Happy_MatchingIfNoneMatch_SkipsOpeningTheStream — 304 shape (Content is null, ETag echoed) + DidNotReceive().OpenReadAsync.
    • Happy_MatchingIfNoneMatch_SkipsOpeningTheThumbnail — same with OpenThumbnailAsync.
    • Happy_MatchingIfNoneMatch_SkipsReadAndCompression — the expensive path: DidNotReceive on both OpenReadAsync and CompressAsync, with MaxBytes: 512 * 1024"{sha}-512" — and I traced the expectation against ImageETag.From (maxKb = maxBytes / 1024 = 512): the string matches the real formatter, not a wish.
    • Happy_NullSha256_ReturnsStreamWithoutETag — the etag is not null && false arm: ETag is null, Assert.Same(stream, Content). The guard's dark side has a light on it now~

    And because green CI has lied to me before, I mutation-probed it: I moved OpenReadAsync before the ETag check in GetImageUseCase — the exact regression these tests exist to catch — and ran the suite: Failed: 1, Passed: 26 — precisely the new pin went red. Restored the file (verified byte-identical via git), re-ran: 57/57 green (ImageUseCaseTests + ImageEndpointsIntegrationTests). These are directional tests, not tautologies. I'm satisfied~ ♡

💡 Still open (non-blocking, as declared)

  1. Shared IntegrationTestServerFactory for the 8 TestHost bootstrap copies — follow-up material, unchanged.
  2. Cache-Control: public behind bearer auth — on record as conscious for the LAN/WireGuard deployment, unchanged.

What I liked~

  • Zero production driftgit diff 482b4c9..369faa1 touches exactly 2 test files, +83/−0. The fix is shaped like the ask.
  • Test comments explain the why ("neither the source read nor the (expensive) recompression ever happened") — these will survive refactors as documentation, not just assertions.
  • The null-arm test uses Assert.Same — pinning that the actual stream from storage flows through, not just any stream. Details~ ♪

Ready to merge from me. Fly~ ♡


Automated review by Jibril · 2026-08-15
CI/CD: stale for head 369faa1 (coverage bot 6233 covers prior 482b4c9 only) · Local checks: mutation probe RED→restore→57/57 green, full targeted RestAdapter suite pass

## 🔮 fufu~ Jibril reviewed your code! Oh? OH! You came back with *exactly* what I asked for — and then some~ ♪ Four surgical unit tests, the null-arm, the budget-suffixed `-512` validator pinned, AND you mirrored the thumbnail 304 asserts I'd filed as a nicety. You even fixed the PR body's "division by zero" wording to say what actually happened (`∞ → (int)` truncation). Fufu~ when someone takes a blade to their own follow-ups unprompted, my wings flutter~ ♡ ### Verdict: ✅ Looks good to me~ #### ⛔ Blocker check — closed and *proven* 1. **`ImageUseCaseTests.cs` — the skip-work contract is now pinned, and I checked it bites.** All four tests from my prescription are there, matching the file's NSubstitute house style exactly: - `Happy_MatchingIfNoneMatch_SkipsOpeningTheStream` — 304 shape (`Content is null`, ETag echoed) + `DidNotReceive().OpenReadAsync`. - `Happy_MatchingIfNoneMatch_SkipsOpeningTheThumbnail` — same with `OpenThumbnailAsync`. - `Happy_MatchingIfNoneMatch_SkipsReadAndCompression` — the expensive path: `DidNotReceive` on **both** `OpenReadAsync` and `CompressAsync`, with `MaxBytes: 512 * 1024` → `"{sha}-512"` — and I traced the expectation against `ImageETag.From` (`maxKb = maxBytes / 1024 = 512`): the string matches the real formatter, not a wish. - `Happy_NullSha256_ReturnsStreamWithoutETag` — the `etag is not null &&` false arm: `ETag is null`, `Assert.Same(stream, Content)`. The guard's dark side has a light on it now~ **And because green CI has lied to me before, I mutation-probed it:** I moved `OpenReadAsync` *before* the ETag check in `GetImageUseCase` — the exact regression these tests exist to catch — and ran the suite: `Failed: 1, Passed: 26` — precisely the new pin went red. Restored the file (verified byte-identical via git), re-ran: **57/57 green** (ImageUseCaseTests + ImageEndpointsIntegrationTests). These are directional tests, not tautologies. I'm satisfied~ ♡ #### 💡 Still open (non-blocking, as declared) 1. Shared `IntegrationTestServerFactory` for the 8 TestHost bootstrap copies — follow-up material, unchanged. 2. `Cache-Control: public` behind bearer auth — on record as conscious for the LAN/WireGuard deployment, unchanged. #### ✅ What I liked~ - **Zero production drift** — `git diff 482b4c9..369faa1` touches exactly 2 test files, +83/−0. The fix is shaped like the ask. - **Test comments explain the *why*** ("neither the source read nor the (expensive) recompression ever happened") — these will survive refactors as documentation, not just assertions. - **The null-arm test uses `Assert.Same`** — pinning that the *actual* stream from storage flows through, not just any stream. Details~ ♪ Ready to merge from me. Fly~ ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: stale for head `369faa1` (coverage bot 6233 covers prior `482b4c9` only) · Local checks: mutation probe RED→restore→57/57 green, full targeted RestAdapter suite pass*
bjoern merged commit 44c1820833 into main 2026-08-15 11:22:53 +02:00
bjoern deleted branch feat/backend-http-caching 2026-08-15 11:22:54 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!68
No description provided.