feat: structured boolean search (Phase 5) #9

Merged
bjoern merged 2 commits from feat/structured-search into main 2026-06-28 21:37:29 +02:00
Member

Implements POST /api/doujins/search with a flat, AND-combined filter model per ADR-0008.

Query shape

POST /api/doujins/search
{
  "text": "search term",
  "anyLanguageCode": ["en", "none"],
  "includeTags": ["action", "romance"],
  "excludeTags": ["horror"],
  "personIds": ["guid..."],
  "personRole": "Artist",
  "circleIds": ["guid..."],
  "minRating": 3,
  "maxRating": 5,
  "unrated": false,
  "variantKind": "Translated",
  "page": 1,
  "pageSize": 20
}

All filters AND-combined. Languages within anyLanguageCode are OR-combined. Include tags are AND (must have all). Exclude tags are NOT-ANY (must not have any).

Supported filters

Filter Description
text Case-insensitive search across title values + description
anyLanguageCode Doujin has a variant with any of these languages
includeTags Doujin has ALL these tags (by normalized name)
excludeTags Doujin does NOT have ANY of these tags
personIds + personRole Doujin has any of these people, optionally filtered by role
circleIds Doujin has any of these circles
minRating / maxRating Rating range filter
unrated Only unrated doujins (Rating is null)
variantKind Doujin has a variant of this kind
page / pageSize Pagination

Results include matchedVariants (only variants matching language/kind filters) and matchedTags.

Architecture

POST /api/doujins/search → SearchDoujinsUseCase → SearchService → EF Core

Server-side IQueryable<Doujin> with progressive filters — no client-side evaluation.

Tests (47 new, 220 total)

Comprehensive integration tests with 4 seeded doujins covering every filter individually and in combination:

  • Text search (title, description, case-insensitive, partial, no-match)
  • Language (single, multi-OR, matched-variant filtering)
  • Include tags (single, multi-AND, case-insensitive)
  • Exclude tags + include/exclude combo
  • Person (any, role-filtered, multi-OR)
  • Circle
  • Rating (min, max, range, excludes-unrated)
  • Unrated
  • Variant kind
  • Combined filters (language + tags + rating, person + circle)
  • Pagination (page 1/2/3, navigation links)
  • Empty body (returns all), no-match (returns empty)
  • matchedVariants/matchedTags population
  • Validation (bad rating/page/role/kind → 400, no auth → 401)
## Phase 5: Structured Boolean Search Implements `POST /api/doujins/search` with a flat, AND-combined filter model per ADR-0008. ### Query shape ```json POST /api/doujins/search { "text": "search term", "anyLanguageCode": ["en", "none"], "includeTags": ["action", "romance"], "excludeTags": ["horror"], "personIds": ["guid..."], "personRole": "Artist", "circleIds": ["guid..."], "minRating": 3, "maxRating": 5, "unrated": false, "variantKind": "Translated", "page": 1, "pageSize": 20 } ``` All filters AND-combined. Languages within `anyLanguageCode` are OR-combined. Include tags are AND (must have all). Exclude tags are NOT-ANY (must not have any). ### Supported filters | Filter | Description | |--------|-------------| | `text` | Case-insensitive search across title values + description | | `anyLanguageCode` | Doujin has a variant with any of these languages | | `includeTags` | Doujin has ALL these tags (by normalized name) | | `excludeTags` | Doujin does NOT have ANY of these tags | | `personIds` + `personRole` | Doujin has any of these people, optionally filtered by role | | `circleIds` | Doujin has any of these circles | | `minRating` / `maxRating` | Rating range filter | | `unrated` | Only unrated doujins (Rating is null) | | `variantKind` | Doujin has a variant of this kind | | `page` / `pageSize` | Pagination | Results include `matchedVariants` (only variants matching language/kind filters) and `matchedTags`. ### Architecture ``` POST /api/doujins/search → SearchDoujinsUseCase → SearchService → EF Core ``` Server-side `IQueryable<Doujin>` with progressive filters — no client-side evaluation. ### Tests (47 new, 220 total) Comprehensive integration tests with 4 seeded doujins covering every filter individually and in combination: - Text search (title, description, case-insensitive, partial, no-match) - Language (single, multi-OR, matched-variant filtering) - Include tags (single, multi-AND, case-insensitive) - Exclude tags + include/exclude combo - Person (any, role-filtered, multi-OR) - Circle - Rating (min, max, range, excludes-unrated) - Unrated - Variant kind - Combined filters (language + tags + rating, person + circle) - Pagination (page 1/2/3, navigation links) - Empty body (returns all), no-match (returns empty) - matchedVariants/matchedTags population - Validation (bad rating/page/role/kind → 400, no auth → 401)
feat: structured boolean search for doujins (Phase 5)
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 36s
db4c1f33d5
Implements POST /api/doujins/search with a flat, AND-combined filter model
per ADR-0008:

- text/title search (title values + description, case-insensitive via
  EF.Functions.Like)
- language filter (doujin has a variant matching ANY language code, OR-combined)
- include tags (must have ALL by normalizedName, AND-combined)
- exclude tags (must NOT have ANY)
- person filter (by ID, optionally constrained by role)
- circle filter (by ID)
- rating range (minRating / maxRating)
- unrated filter (Rating is null)
- variant kind filter
- pagination (page, pageSize, with totalItems / totalPages)

Each SearchResult returns the matched doujin plus the variants that matched
the active language/kind filters (all variants when no such filter is set).

New files:
  - ApplicationCore/UseCases/SearchUseCases.cs (query/result models + interface)
  - ApplicationCore/Services/ISearchService.cs
  - Infrastructure/Services/SearchService.cs (EF Core translation)
  - Infrastructure/UseCases/SearchUseCases.cs
  - RestAdapter/Dtos/SearchDtos.cs
  - RestAdapter/Endpoints/SearchEndpoints.cs
  - tests/.../Endpoints/SearchIntegrationTests.cs (47 new tests)

Registration: UseCaseRegistrationHelper + Program.cs map the new endpoint.

All 220 tests pass (173 existing + 47 new).

Summary

Summary
Generated on: 06/28/2026 - 19:27:02
Coverage date: 06/28/2026 - 19:26:46 - 06/28/2026 - 19:27:00
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 201
Files: 88
Line coverage: 83.9% (4117 of 4902)
Covered lines: 4117
Uncovered lines: 785
Coverable lines: 4902
Total lines: 8355
Branch coverage: 50% (360 of 720)
Covered branches: 360
Total branches: 720
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 84.8%
Name Line Branch
DoujinManager.ApplicationCore 84.8% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 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.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.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 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.AddTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 0%
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.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 0%
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.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 91.8%
Name Line Branch
DoujinManager.Infrastructure 91.8% 71.1%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 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.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.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 69.2%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 88.2% 70.9%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.ChapterService 54.2% 25%
DoujinManager.Infrastructure.Services.CircleService 69.6% 0%
DoujinManager.Infrastructure.Services.DoujinService 69.5% 45.8%
DoujinManager.Infrastructure.Services.ImageService 91% 50%
DoujinManager.Infrastructure.Services.PageService 84.9% 65%
DoujinManager.Infrastructure.Services.PersonService 69.6% 0%
DoujinManager.Infrastructure.Services.SearchService 100% 100%
DoujinManager.Infrastructure.Services.TagService 94.7% 100%
DoujinManager.Infrastructure.Services.VariantService 50.7% 16.6%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95% 75%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 100% 98%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 0%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 90.9% 75%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 0%
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.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 0%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 0%
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.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 92.8% 92.8%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 74.1% 62.5%
DoujinManager.RestAdapter - 83.3%
Name Line Branch
DoujinManager.RestAdapter 83.3% 76.4%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Dtos.AssignTagDto 0%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 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.LinkCircleDto 0%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 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.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 0%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 81.2% 87.5%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 96.6% 50%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.2%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 50%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 69.9% 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 86.1% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
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% 50%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 78.6% 82.6%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 87.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 18.5%
Name Line Branch
DoujinManager.Server 18.5% 0%
DoujinManager.Server.ImageInfrastructureRegistration 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: | 06/28/2026 - 19:27:02 | | Coverage date: | 06/28/2026 - 19:26:46 - 06/28/2026 - 19:27:00 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 201 | | Files: | 88 | | **Line coverage:** | 83.9% (4117 of 4902) | | Covered lines: | 4117 | | Uncovered lines: | 785 | | Coverable lines: | 4902 | | Total lines: | 8355 | | **Branch coverage:** | 50% (360 of 720) | | Covered branches: | 360 | | Total branches: | 720 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 84.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**84.8%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|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.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.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|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.AddTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|0%|| |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.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|0%|| |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.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |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 - 91.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**91.8%**|**71.1%**| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|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.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.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|69.2%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|88.2%|70.9%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.ChapterService|54.2%|25%| |DoujinManager.Infrastructure.Services.CircleService|69.6%|0%| |DoujinManager.Infrastructure.Services.DoujinService|69.5%|45.8%| |DoujinManager.Infrastructure.Services.ImageService|91%|50%| |DoujinManager.Infrastructure.Services.PageService|84.9%|65%| |DoujinManager.Infrastructure.Services.PersonService|69.6%|0%| |DoujinManager.Infrastructure.Services.SearchService|100%|100%| |DoujinManager.Infrastructure.Services.TagService|94.7%|100%| |DoujinManager.Infrastructure.Services.VariantService|50.7%|16.6%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95%|75%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|100%|98%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|0%|| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|90.9%|75%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|0%|| |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.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|0%|| |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.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|92.8%|92.8%| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|74.1%|62.5%| </details> <details><summary>DoujinManager.RestAdapter - 83.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**83.3%**|**76.4%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Dtos.AssignTagDto|0%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|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.LinkCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|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.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|0%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|81.2%|87.5%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|96.6%|50%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.2%|| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|50%| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|69.9%|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|86.1%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |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%|50%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|78.6%|82.6%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|87.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 18.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**18.5%**|**0%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Author
Member

🤖 Hermes automated review: minor comments

Reviewed the full diff (1065 additions, 9 files) for the structured boolean search feature. This is a well-structured, high-quality implementation — clean architecture (DTO → UseCase → Service → server-side IQueryable), no client-side evaluation, comprehensive test coverage (47 new tests, 220 total). No blocking issues found. A few minor non-blocking notes below.

⚠️ Note: this is a PR conversation comment, not a formal Forgejo review approval. The current MCP integration cannot create inline diff comments or approval states.


Minor (non-blocking) observations

1. Text search does not escape LIKE wildcards in user inputbackend/src/DoujinManager.Infrastructure/Services/SearchService.cs:~52

var pattern = $"%{text}%";
queryable = queryable.Where(d =>
    d.Titles.Any(t => EF.Functions.Like(t.Value, pattern)) || ...

The % and _ characters in a user's search text act as SQL wildcards, so searching for e.g. 100% or a_b would match more than intended. This is not a SQL injection issue — EF.Functions.Like is parameterized — just a correctness/UX edge case. Suggested fix:

var escaped = text.Replace(@"\", @"\\").Replace("%", @"\%").Replace("_", @"\_");
var pattern = $"%{escaped}%";
// pass the escape char: EF.Functions.Like(t.Value, pattern, @"\")

Low severity; titles/descriptions rarely contain these chars.

2. matchedTags returns the doujin's full tag set, not only the matched tagsbackend/src/DoujinManager.RestAdapter/Endpoints/SearchEndpoints.cs:~95

var matchedTags = d.Tags
    .Select(t => new TagDto(t.Tag.Id.Value, t.Tag.Name, t.Tag.Namespace))
    .ToList();

The field is named matchedTags, but this projects all of the doujin's tags regardless of the includeTags filter. The test Search_MatchedTags_Populated confirms A returns both action and romance when filtered only by action. If the intent is to return all tags, consider renaming to tags for clarity; if the intent is only the matched ones, intersect against includeTags.

3. unrated: false is an intentional no-op — confirmed by Search_By_Unrated_False_Does_Not_Filter and the if (query.Unrated is { } unrated && unrated) guard. This tri-state design (null/true/false) is fine and tested; just noting it's worth a one-line note in the SearchDoujinsDto XML doc so callers know false ≡ unset.

What looks good

  • Correct AND-combining of include tags via per-iteration Where (with the loop-variable capture handled correctly).
  • OrderByDescending(UpdatedAt).ThenBy(Id) gives deterministic pagination with a tiebreaker.
  • Pagination clamping (Math.Max(1, page), Math.Clamp(pageSize, 1, 100)) and totalPages math are correct, including the totalItems == 0 → 0 pages edge.
  • Input normalization (trim/lowercase/distinct, Guid.Empty filtering) done once before query construction.
  • Rating filters correctly exclude nulls (d.Rating != null && ...) — tested via Search_By_MinRating_Excludes_Unrated.
  • Validation, auth (401), and bad-enum (400) paths all tested.
  • Registration wired in Program.cs (MapSearchEndpoints) and UseCaseRegistrationHelper for both service and use case.

Verification

  • CI/CD: Forgejo Actions coverage comment for head db4c1f33 generated successfully (line 83.9%, SearchService 100%/100%, SearchEndpoints 100%/50%). This implies build + 220 tests passed. Local build/test skipped per CI-evidence policy.
  • Branch coverage note: the 50% on SearchEndpoints reflects the enum-parse null/empty branches — the error paths themselves are covered by the Unknown_*_Returns_400 tests.

Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: minor comments Reviewed the full diff (1065 additions, 9 files) for the structured boolean search feature. This is a well-structured, high-quality implementation — clean architecture (DTO → UseCase → Service → server-side `IQueryable`), no client-side evaluation, comprehensive test coverage (47 new tests, 220 total). No blocking issues found. A few minor non-blocking notes below. > ⚠️ Note: this is a PR conversation comment, not a formal Forgejo review approval. The current MCP integration cannot create inline diff comments or approval states. --- ### Minor (non-blocking) observations **1. Text search does not escape LIKE wildcards in user input** — `backend/src/DoujinManager.Infrastructure/Services/SearchService.cs:~52` ```csharp var pattern = $"%{text}%"; queryable = queryable.Where(d => d.Titles.Any(t => EF.Functions.Like(t.Value, pattern)) || ... ``` The `%` and `_` characters in a user's search text act as SQL wildcards, so searching for e.g. `100%` or `a_b` would match more than intended. This is **not** a SQL injection issue — `EF.Functions.Like` is parameterized — just a correctness/UX edge case. Suggested fix: ```csharp var escaped = text.Replace(@"\", @"\\").Replace("%", @"\%").Replace("_", @"\_"); var pattern = $"%{escaped}%"; // pass the escape char: EF.Functions.Like(t.Value, pattern, @"\") ``` Low severity; titles/descriptions rarely contain these chars. **2. `matchedTags` returns the doujin's *full* tag set, not only the matched tags** — `backend/src/DoujinManager.RestAdapter/Endpoints/SearchEndpoints.cs:~95` ```csharp var matchedTags = d.Tags .Select(t => new TagDto(t.Tag.Id.Value, t.Tag.Name, t.Tag.Namespace)) .ToList(); ``` The field is named `matchedTags`, but this projects *all* of the doujin's tags regardless of the `includeTags` filter. The test `Search_MatchedTags_Populated` confirms A returns both `action` and `romance` when filtered only by `action`. If the intent is to return all tags, consider renaming to `tags` for clarity; if the intent is only the matched ones, intersect against `includeTags`. **3. `unrated: false` is an intentional no-op** — confirmed by `Search_By_Unrated_False_Does_Not_Filter` and the `if (query.Unrated is { } unrated && unrated)` guard. This tri-state design (null/true/false) is fine and tested; just noting it's worth a one-line note in the `SearchDoujinsDto` XML doc so callers know `false` ≡ unset. ### What looks good - Correct AND-combining of include tags via per-iteration `Where` (with the loop-variable capture handled correctly). - `OrderByDescending(UpdatedAt).ThenBy(Id)` gives deterministic pagination with a tiebreaker. ✅ - Pagination clamping (`Math.Max(1, page)`, `Math.Clamp(pageSize, 1, 100)`) and `totalPages` math are correct, including the `totalItems == 0 → 0 pages` edge. - Input normalization (trim/lowercase/distinct, `Guid.Empty` filtering) done once before query construction. - Rating filters correctly exclude nulls (`d.Rating != null && ...`) — tested via `Search_By_MinRating_Excludes_Unrated`. - Validation, auth (401), and bad-enum (400) paths all tested. - Registration wired in `Program.cs` (`MapSearchEndpoints`) and `UseCaseRegistrationHelper` for both service and use case. ### Verification - **CI/CD:** Forgejo Actions coverage comment for head `db4c1f33` generated successfully (line 83.9%, `SearchService` 100%/100%, `SearchEndpoints` 100%/50%). This implies build + 220 tests passed. Local build/test skipped per CI-evidence policy. - Branch coverage note: the 50% on `SearchEndpoints` reflects the enum-parse null/empty branches — the error paths themselves are covered by the `Unknown_*_Returns_400` tests. *Automated daily review. I never merge PRs.*
fix: escape LIKE wildcards, rename matchedTags→tags, document unrated
All checks were successful
CI / build (pull_request) Successful in 18s
CI / test (pull_request) Successful in 36s
fd39ae577f
Address 3 minor review findings:

1. Text search now escapes SQL LIKE wildcards (%, _, \) in user input
   so they're treated literally, not as pattern matchers.

2. Renamed matchedTags → tags in SearchResultDto since it returns ALL
   the doujin's tags, not just the ones matching the includeTags filter.

3. Added XML doc on Unrated field: 'If true, restricts to doujins with
   no rating. Null or false = no filter.' Clarifies the tri-state design.
Author
Member

Addressed all 3 minor findings in commit fd39ae5. 220 tests pass.

  1. LIKE wildcard escapingSearchService now escapes %, _, and \ in user search text and passes \ as the escape char to EF.Functions.Like. Searching for 100% or a_b now matches literally.

  2. matchedTagstags — Renamed to tags in SearchResultDto and JSON output, since it returns the doujin's full tag set. Updated endpoint and test accordingly.

  3. unrated doc — Added XML doc: "If true, restricts to doujins with no rating (Rating is null). Null or false = no filter."

Addressed all 3 minor findings in commit `fd39ae5`. 220 tests pass. 1. **LIKE wildcard escaping** — `SearchService` now escapes `%`, `_`, and `\` in user search text and passes `\` as the escape char to `EF.Functions.Like`. Searching for `100%` or `a_b` now matches literally. 2. **`matchedTags` → `tags`** — Renamed to `tags` in `SearchResultDto` and JSON output, since it returns the doujin's full tag set. Updated endpoint and test accordingly. 3. **`unrated` doc** — Added XML doc: *"If true, restricts to doujins with no rating (Rating is null). Null or false = no filter."*
Author
Member

🤖 Hermes automated review: no blocking issues found

Follow-up review of the fix commit fd39ae5 ("escape LIKE wildcards, rename matchedTags→tags, document unrated"), which addresses all three minor findings from the prior review. The changes are correct, minimal, and well-scoped. No blocking issues.

⚠️ Note: this is a PR conversation comment, not a formal Forgejo review approval. The current MCP integration cannot create inline diff comments or approval states.


Findings from prior review — all resolved

1. LIKE wildcard escapingbackend/src/DoujinManager.Infrastructure/Services/SearchService.cs:55-62
Now escapes \, then %, then _ before building the pattern, and passes \ as the escape char to EF.Functions.Like(..., @"\"). Order is correct (backslash first, so escaping itself doesn't re-introduce wildcards). Searching for 100% or a_b now matches literally.

2. matchedTagstagsbackend/src/DoujinManager.RestAdapter/Dtos/SearchDtos.cs:66-67, SearchEndpoints.cs:100-104
Renamed the DTO field and JSON property (matchedTagstags) and the local variable. Since the field returns the doujin's full tag set, tags is accurate. Test Search_MatchedTags_Populated renamed to Search_Tags_Populated and assertions updated to a.Tags.

3. unrated docSearchDtos.cs:43
Added XML doc: "If true, restricts to doujins with no rating (Rating is null). Null or false = no filter." Clearly documents the tri-state behavior.

Verification

  • CI/CD: The Forgejo Actions coverage comment on this PR (issuecomment-204) reports data for head db4c1f33 (line 83.9%, SearchService 100%/100%), but the head has since moved to fd39ae57. The coverage comment was last updated at 21:27 but its "Coverage date" field still reads 19:26:46 — i.e., it predates the fix commit. Treating CI as stale for the current head; ran tests locally per policy.
  • Local: dotnet testPassed: 92, Failed: 0 (net10.0, 33s). Includes the full SearchIntegrationTests suite (47 search tests) with the renamed Search_Tags_Populated test. Only pre-existing nullable-warning diagnostics, no errors.

Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: no blocking issues found Follow-up review of the fix commit `fd39ae5` ("escape LIKE wildcards, rename matchedTags→tags, document unrated"), which addresses all three minor findings from the prior review. The changes are correct, minimal, and well-scoped. No blocking issues. > ⚠️ Note: this is a PR conversation comment, not a formal Forgejo review approval. The current MCP integration cannot create inline diff comments or approval states. --- ### Findings from prior review — all resolved ✅ **1. LIKE wildcard escaping** — `backend/src/DoujinManager.Infrastructure/Services/SearchService.cs:55-62` Now escapes `\`, then `%`, then `_` before building the pattern, and passes `\` as the escape char to `EF.Functions.Like(..., @"\")`. Order is correct (backslash first, so escaping itself doesn't re-introduce wildcards). Searching for `100%` or `a_b` now matches literally. ✅ **2. `matchedTags` → `tags`** — `backend/src/DoujinManager.RestAdapter/Dtos/SearchDtos.cs:66-67`, `SearchEndpoints.cs:100-104` Renamed the DTO field and JSON property (`matchedTags` → `tags`) and the local variable. Since the field returns the doujin's full tag set, `tags` is accurate. Test `Search_MatchedTags_Populated` renamed to `Search_Tags_Populated` and assertions updated to `a.Tags`. ✅ **3. `unrated` doc** — `SearchDtos.cs:43` Added XML doc: *"If true, restricts to doujins with no rating (Rating is null). Null or false = no filter."* Clearly documents the tri-state behavior. ✅ ### Verification - **CI/CD:** The Forgejo Actions coverage comment on this PR (issuecomment-204) reports data for head `db4c1f33` (line 83.9%, `SearchService` 100%/100%), but the head has since moved to `fd39ae57`. The coverage comment was last updated at 21:27 but its "Coverage date" field still reads 19:26:46 — i.e., it predates the fix commit. Treating CI as **stale for the current head**; ran tests locally per policy. - **Local:** `dotnet test` → **Passed: 92, Failed: 0** (net10.0, 33s). Includes the full `SearchIntegrationTests` suite (47 search tests) with the renamed `Search_Tags_Populated` test. Only pre-existing nullable-warning diagnostics, no errors. *Automated daily review. I never merge PRs.*
bjoern merged commit ddf232db0b into main 2026-06-28 21:37:29 +02:00
bjoern deleted branch feat/structured-search 2026-06-28 21:37:29 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 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!9
No description provided.