feat: hypermedia links and actions across all REST endpoints (Phase 4) #8

Merged
bjoern merged 2 commits from feat/hypermedia-responses into main 2026-06-28 20:17:40 +02:00
Member

Replaces all EnvelopeDefaults.EmptyLinks/EmptyActions with actual hypermedia data across every REST endpoint, per ADR-0007.

What changed

New: HypermediaHelpers.cs — Reusable static builders for every resource type:

  • BuildCollectionLinks (self/next/prev pagination) and BuildCollectionActions (create)
  • Resource-specific link/action builders for Doujin, Variant, Chapter, Page, Tag, Person, Circle
  • Nested-collection variants (variants under doujin, chapters/pages under variant)

Updated: All 4 endpoint files — Every ResourceResponse and CollectionResponse now returns proper links and actions:

Resource Links Actions
Doujin self, variants update, delete, addTitle, addVariant, assignTag, assignPerson, assignCircle
Variant self, doujin, pages, chapters update, delete, uploadPages, uploadZip, addChapter, reorderPages
Chapter self, variant update, delete
Page self, image, thumbnail delete
Tag/Person/Circle self update, delete
Collections self, next, prev, parent create (or upload/reorder for pages)

Example response

{
  "data": { "id": "...", "title": "..." },
  "links": {
    "self": { "href": "/api/doujins/abc", "method": "GET" },
    "variants": { "href": "/api/doujins/abc/variants", "method": "GET" }
  },
  "actions": {
    "update": { "name": "update", "method": "PUT", "href": "/api/doujins/abc", "type": "application/json" },
    "delete": { "name": "delete", "method": "DELETE", "href": "/api/doujins/abc" },
    "addVariant": { "name": "addVariant", "method": "POST", "href": "/api/doujins/abc/variants", "type": "application/json" }
  }
}

Tests (23 new, 173 total)

  • 14 unit tests for HypermediaHelpers — pagination logic, all resource link/action sets, multipart encoding types
  • 8 integration tests verifying links/actions in actual HTTP responses
  • Updated existing tests to assert links/actions are non-empty

Zero remaining EnvelopeDefaults.EmptyLinks/EmptyActions in endpoint code.

## Phase 4: Hypermedia Links and Actions Replaces all `EnvelopeDefaults.EmptyLinks`/`EmptyActions` with actual hypermedia data across every REST endpoint, per ADR-0007. ### What changed **New: `HypermediaHelpers.cs`** — Reusable static builders for every resource type: - `BuildCollectionLinks` (self/next/prev pagination) and `BuildCollectionActions` (create) - Resource-specific link/action builders for Doujin, Variant, Chapter, Page, Tag, Person, Circle - Nested-collection variants (variants under doujin, chapters/pages under variant) **Updated: All 4 endpoint files** — Every `ResourceResponse` and `CollectionResponse` now returns proper links and actions: | Resource | Links | Actions | |----------|-------|---------| | Doujin | self, variants | update, delete, addTitle, addVariant, assignTag, assignPerson, assignCircle | | Variant | self, doujin, pages, chapters | update, delete, uploadPages, uploadZip, addChapter, reorderPages | | Chapter | self, variant | update, delete | | Page | self, image, thumbnail | delete | | Tag/Person/Circle | self | update, delete | | Collections | self, next, prev, parent | create (or upload/reorder for pages) | ### Example response ```json { "data": { "id": "...", "title": "..." }, "links": { "self": { "href": "/api/doujins/abc", "method": "GET" }, "variants": { "href": "/api/doujins/abc/variants", "method": "GET" } }, "actions": { "update": { "name": "update", "method": "PUT", "href": "/api/doujins/abc", "type": "application/json" }, "delete": { "name": "delete", "method": "DELETE", "href": "/api/doujins/abc" }, "addVariant": { "name": "addVariant", "method": "POST", "href": "/api/doujins/abc/variants", "type": "application/json" } } } ``` ### Tests (23 new, 173 total) - **14 unit tests** for `HypermediaHelpers` — pagination logic, all resource link/action sets, multipart encoding types - **8 integration tests** verifying links/actions in actual HTTP responses - **Updated existing tests** to assert links/actions are non-empty Zero remaining `EnvelopeDefaults.EmptyLinks/EmptyActions` in endpoint code.
feat: hypermedia links and actions across all REST endpoints
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 27s
64d3f4fced
Replace EnvelopeDefaults.EmptyLinks/EmptyActions with actual hypermedia
data in every endpoint response per the ADR-0007 hypermedia map.

New file: RestAdapter/Envelopes/HypermediaHelpers.cs
- Static class with reusable Build{Resource}Links / Build{Resource}Actions
  builders for doujins, variants, chapters, pages, titles, tags, people,
  circles, plus generic collection link/action builders.
- All methods have XML doc comments.

Endpoints updated:
- DoujinEndpoints: collection + resource + title creation now emit full
  link/action sets (self, variants, titles; update/delete/addTitle/
  addVariant/assignTag/assignPerson/assignCircle).
- VariantEndpoints: nested collection + create, flat GET/PUT, and chapter
  create/update/list now emit hypermedia. Variant links include doujin,
  pages, chapters; actions include uploadPages/uploadZip (multipart) and
  reorderPages.
- MetadataEndpoints: tags, people, circles collections and create/update
  endpoints now emit hypermedia.
- ImageEndpoints: page collection now emits collection links + upload/
  reorder actions.
- DoujinEndpoints.BuildDoujinLinks/Actions now delegate to HypermediaHelpers
  (kept for backward compatibility).

Tests:
- New HypermediaHelpersTests.cs: 14 unit tests covering every builder.
- New HypermediaIntegrationTests.cs: 8 integration tests verifying
  links/actions appear in actual HTTP responses for doujin/variant/chapter/
  tag/page resources and collections.
- DoujinCrudIntegrationTests.cs: added assertions that links/actions are
  non-empty on doujin create/get, variant create, and the empty collection
  response.

Build: 0 errors, 0 warnings. Tests: 173 passed (150 existing + 23 new).

Summary

Summary
Generated on: 06/28/2026 - 17:49:38
Coverage date: 06/28/2026 - 17:49:31 - 06/28/2026 - 17:49:36
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 193
Files: 83
Line coverage: 82.7% (3791 of 4584)
Covered lines: 3791
Uncovered lines: 793
Coverable lines: 4584
Total lines: 7839
Branch coverage: 45.2% (298 of 658)
Covered branches: 298
Total branches: 658
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 81.9%
Name Line Branch
DoujinManager.ApplicationCore 81.9% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCircle 0%
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.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.3%
Name Line Branch
DoujinManager.Infrastructure 91.3% 66%
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 93.7%
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 66.6% 0%
DoujinManager.Infrastructure.Services.DoujinService 66.6% 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.TagService 94.7% 50%
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.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 - 81%
Name Line Branch
DoujinManager.RestAdapter 81% 76%
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.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 80.4% 75%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 96.6% 50%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.2%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
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 74.8% 81.8%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 87.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 18%
Name Line Branch
DoujinManager.Server 18% 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 - 17:49:38 | | Coverage date: | 06/28/2026 - 17:49:31 - 06/28/2026 - 17:49:36 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 193 | | Files: | 83 | | **Line coverage:** | 82.7% (3791 of 4584) | | Covered lines: | 3791 | | Uncovered lines: | 793 | | Coverable lines: | 4584 | | Total lines: | 7839 | | **Branch coverage:** | 45.2% (298 of 658) | | Covered branches: | 298 | | Total branches: | 658 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 81.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**81.9%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|0%|| |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.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.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**91.3%**|**66%**| |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|93.7%|| |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|66.6%|0%| |DoujinManager.Infrastructure.Services.DoujinService|66.6%|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.TagService|94.7%|50%| |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.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 - 81%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**81%**|**76%**| |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.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|80.4%|75%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|96.6%|50%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.2%|| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |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|74.8%|81.8%| |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%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**18%**|**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: changes requested

Reviewed the full diff (8 files, +841/−42) for head 64d3f4fc against base 67e37fda. The overall structure of HypermediaHelpers.cs is clean and the unit tests are thorough on shape, but cross-referencing every emitted href against the actual MapGroup(...) route registrations in the four endpoint files reveals broken hypermedia links — the core correctness property this PR exists to provide. These are served in live HTTP responses, so they should be fixed before merge.

🔴 Major — dead/wrong hrefs served in responses

1. HypermediaHelpers.cs:66BuildDoujinLinks adds a titles link that 404s
BuildDoujinLinks emits ["titles"] = new("/api/doujins/{id}/titles", "GET"), and this builder is served by the GET /api/doujins/{id} handler (DoujinEndpoints.cs:42). But there is no GET /api/doujins/{id}/titles route — only POST /{id:guid}/titles and DELETE /{id:guid}/titles/{titleId} under the /api/doujins group. This PR introduces the dead link (the old private BuildDoujinLinks only had self/variants). Clients following it will get a 404.
Suggested fix: either drop the titles entry (titles are already embedded in the doujin detail DTO) or add the missing GET route.

2. HypermediaHelpers.cs:146 — chapter self link points to a non-existent route
BuildChapterLinks emits ["self"] = new("/api/chapters/{id}", "GET"), but there is no /api/chapters route group at all. Chapters are only reachable via the nested group /api/variants/{variantId}/chapters (list). This builder is actively served on chapter create (201, VariantEndpoints.cs:108) and update (200, VariantEndpoints.cs:120), so every chapter mutation response advertises a self link that 404s.

3. HypermediaHelpers.cs:156-157 — chapter update/delete actions use the wrong path
BuildChapterActions emits PUT /api/chapters/{id} and DELETE /api/chapters/{id}, but the real routes are PUT /api/variants/{variantId}/chapters/{chapterId} and DELETE /api/variants/{variantId}/chapters/{chapterId} (VariantEndpoints.cs:118,131). The hrefs are missing the entire variants/{variantId} prefix, so a hypermedia client that follows the advertised update/delete action to mutate a chapter will hit a 404 (or, worse, a future unrelated flat route). Served on the same create/update responses as #2.
Suggested fix for #2 and #3: thread variantId into BuildChapterActions (it already takes it for links) and emit the nested /api/variants/{variantId}/chapters/{chapterId} hrefs; for self, either use the nested list route or add a real single-chapter GET.

🟡 Minor (non-blocking)

  • HypermediaHelpers.cs:186BuildPageLinks selfGET /api/pages/{id} has no GET route (the /api/pages group only registers DELETE /{id}). Not currently wired into any endpoint (only the *PageCollection* builders are used in ImageEndpoints.cs), so it's latent — but the unit test BuildPageLinks_IncludesSelfImageAndThumbnail asserts the dead href, which will mislead future callers. Worth aligning if a single-page GET is added.
  • HypermediaHelpers.cs:230BuildTitleLinks(Guid titleId, ...) ignores titleId (XML doc acknowledges this). Fine as-is, just flagging the unused param.

Why the tests didn't catch this

The 14 HypermediaHelpersTests assert the href strings match the builder output (e.g. Assert.Equal("/api/chapters/{SampleId}", links["self"].Href)) — they verify the builder returns what it returns, not that the href resolves to a registered route. The 8 integration tests assert links/actions are non-empty, not that specific hrefs are reachable. A route-existence check (or asserting hrefs against the same constants used in MapGroup) would prevent this class of bug.

Verification

  • CI/CD: Forgejo Actions coverage report present for head 64d3f4fc (generated 06/28 17:18, 4 assemblies, line 82.6% / branch 45.2%, HypermediaHelpers 86% line / 100% branch). Build+test ran successfully for this head; local build/test skipped per CI-evidence policy.
  • Findings above are from static cross-reference of the diff against route registrations; not a build/test failure.

Note: this is a PR conversation comment with file:line references — not a formal Forgejo review approval (the MCP integration cannot create approval states or inline diff comments).


Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: changes requested Reviewed the full diff (8 files, +841/−42) for head `64d3f4fc` against base `67e37fda`. The overall structure of `HypermediaHelpers.cs` is clean and the unit tests are thorough on *shape*, but cross-referencing every emitted href against the actual `MapGroup(...)` route registrations in the four endpoint files reveals **broken hypermedia links** — the core correctness property this PR exists to provide. These are served in live HTTP responses, so they should be fixed before merge. ### 🔴 Major — dead/wrong hrefs served in responses **1. `HypermediaHelpers.cs:66` — `BuildDoujinLinks` adds a `titles` link that 404s** `BuildDoujinLinks` emits `["titles"] = new("/api/doujins/{id}/titles", "GET")`, and this builder is served by the `GET /api/doujins/{id}` handler (`DoujinEndpoints.cs:42`). But there is **no `GET /api/doujins/{id}/titles` route** — only `POST /{id:guid}/titles` and `DELETE /{id:guid}/titles/{titleId}` under the `/api/doujins` group. This PR *introduces* the dead link (the old private `BuildDoujinLinks` only had `self`/`variants`). Clients following it will get a 404. *Suggested fix:* either drop the `titles` entry (titles are already embedded in the doujin detail DTO) or add the missing `GET` route. **2. `HypermediaHelpers.cs:146` — chapter `self` link points to a non-existent route** `BuildChapterLinks` emits `["self"] = new("/api/chapters/{id}", "GET")`, but there is **no `/api/chapters` route group at all**. Chapters are only reachable via the nested group `/api/variants/{variantId}/chapters` (list). This builder is actively served on chapter create (`201`, `VariantEndpoints.cs:108`) and update (`200`, `VariantEndpoints.cs:120`), so every chapter mutation response advertises a `self` link that 404s. **3. `HypermediaHelpers.cs:156-157` — chapter `update`/`delete` actions use the wrong path** `BuildChapterActions` emits `PUT /api/chapters/{id}` and `DELETE /api/chapters/{id}`, but the real routes are `PUT /api/variants/{variantId}/chapters/{chapterId}` and `DELETE /api/variants/{variantId}/chapters/{chapterId}` (`VariantEndpoints.cs:118,131`). The hrefs are missing the entire `variants/{variantId}` prefix, so a hypermedia client that follows the advertised `update`/`delete` action to mutate a chapter will hit a 404 (or, worse, a future unrelated flat route). Served on the same create/update responses as #2. *Suggested fix for #2 and #3:* thread `variantId` into `BuildChapterActions` (it already takes it for links) and emit the nested `/api/variants/{variantId}/chapters/{chapterId}` hrefs; for `self`, either use the nested list route or add a real single-chapter GET. ### 🟡 Minor (non-blocking) - **`HypermediaHelpers.cs:186` — `BuildPageLinks` `self` → `GET /api/pages/{id}` has no GET route** (the `/api/pages` group only registers `DELETE /{id}`). Not currently wired into any endpoint (only the `*PageCollection*` builders are used in `ImageEndpoints.cs`), so it's latent — but the unit test `BuildPageLinks_IncludesSelfImageAndThumbnail` asserts the dead href, which will mislead future callers. Worth aligning if a single-page GET is added. - **`HypermediaHelpers.cs:230` — `BuildTitleLinks(Guid titleId, ...)` ignores `titleId`** (XML doc acknowledges this). Fine as-is, just flagging the unused param. ### Why the tests didn't catch this The 14 `HypermediaHelpersTests` assert the href *strings* match the builder output (e.g. `Assert.Equal("/api/chapters/{SampleId}", links["self"].Href)`) — they verify the builder returns what it returns, not that the href resolves to a registered route. The 8 integration tests assert links/actions are *non-empty*, not that specific hrefs are reachable. A route-existence check (or asserting hrefs against the same constants used in `MapGroup`) would prevent this class of bug. ### ✅ Verification - **CI/CD:** Forgejo Actions coverage report present for head `64d3f4fc` (generated 06/28 17:18, 4 assemblies, line 82.6% / branch 45.2%, `HypermediaHelpers` 86% line / 100% branch). Build+test ran successfully for this head; **local build/test skipped** per CI-evidence policy. - Findings above are from static cross-reference of the diff against route registrations; not a build/test failure. *Note: this is a PR conversation comment with file:line references — not a formal Forgejo review approval (the MCP integration cannot create approval states or inline diff comments).* --- *Automated daily review. I never merge PRs.*
fix: correct all broken hypermedia hrefs
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 27s
37ef835430
Fix 3 major + 1 minor finding from automated review:

1. Doujin 'titles' link removed — no GET /api/doujins/{id}/titles route
   exists. Titles are already embedded in the doujin detail DTO.

2. Chapter links fixed — chapters have no standalone GET route, so
   'self' replaced with 'collection' pointing to the nested chapter
   collection at /api/variants/{variantId}/chapters.

3. Chapter actions fixed — update/delete now correctly use the nested
   path /api/variants/{variantId}/chapters/{chapterId} instead of the
   non-existent /api/chapters/{id}. BuildChapterActions now requires
   variantId parameter.

4. Page links fixed — no GET /api/pages/{id} route exists, so 'self'
   replaced with 'collection' pointing to /api/variants/{variantId}/pages.
   BuildPageLinks now requires variantId parameter.

Updated all unit tests and integration tests to match corrected hrefs.
Author
Member

Fixed all broken hypermedia hrefs in commit 37ef835. 173 tests pass.

Major fixes

#1 — Dead titles link removed
Removed ["titles"] from BuildDoujinLinks. There is no GET /api/doujins/{id}/titles route — titles are embedded in the doujin detail DTO.

#2 — Chapter self link → collection link
Chapters have no standalone GET route. Changed ["self"] = /api/chapters/{id} to ["collection"] = /api/variants/{variantId}/chapters — the actual route that exists.

#3 — Chapter update/delete actions use nested paths
BuildChapterActions now requires variantId and emits:

  • PUT /api/variants/{variantId}/chapters/{chapterId}
  • DELETE /api/variants/{variantId}/chapters/{chapterId}

Minor fix

#4 — Page self link → collection link
No GET /api/pages/{id} route exists. Changed to ["collection"] = /api/variants/{variantId}/pages. BuildPageLinks now requires variantId.

Tests updated

All unit tests and integration tests updated to assert the corrected hrefs. Added Assert.DoesNotContain("titles", ...) to explicitly guard against reintroducing the dead link.

Fixed all broken hypermedia hrefs in commit `37ef835`. 173 tests pass. ### Major fixes **#1 — Dead `titles` link removed** Removed `["titles"]` from `BuildDoujinLinks`. There is no `GET /api/doujins/{id}/titles` route — titles are embedded in the doujin detail DTO. **#2 — Chapter `self` link → `collection` link** Chapters have no standalone GET route. Changed `["self"] = /api/chapters/{id}` to `["collection"] = /api/variants/{variantId}/chapters` — the actual route that exists. **#3 — Chapter `update`/`delete` actions use nested paths** `BuildChapterActions` now requires `variantId` and emits: - `PUT /api/variants/{variantId}/chapters/{chapterId}` - `DELETE /api/variants/{variantId}/chapters/{chapterId}` ### Minor fix **#4 — Page `self` link → `collection` link** No `GET /api/pages/{id}` route exists. Changed to `["collection"] = /api/variants/{variantId}/pages`. `BuildPageLinks` now requires `variantId`. ### Tests updated All unit tests and integration tests updated to assert the corrected hrefs. Added `Assert.DoesNotContain("titles", ...)` to explicitly guard against reintroducing the dead link.
Author
Member

🤖 Hermes automated review: no blocking issues found (fixes verified)

Follow-up review of head 37ef8354 (the fix: correct all broken hypermedia hrefs commit). I statically verified each of the three major + one minor finding from my previous review against the actual MapGroup(...) / MapVerb(...) route registrations.

All previously-reported issues resolved

# Finding Fix in 37ef835 Route verified?
1 Dead titles link in BuildDoujinLinks ["titles"] removed (comment explains why) No GET /api/doujins/{id}/titles route exists (only POST/DELETE) — removal is correct
2 Chapter self → nonexistent /api/chapters/{id} Changed to ["collection"] = /api/variants/{variantId}/chapters GET / registered on that group (VariantEndpoints.cs:87,90)
3 Chapter update/delete missing variants/{variantId} prefix BuildChapterActions now takes variantId; emits nested paths PUT/DELETE /{chapterId} registered under /api/variants/{variantId:guid}/chapters (VariantEndpoints.cs:118,131)
4 (minor) BuildPageLinks self → nonexistent /api/pages/{id} Changed to ["collection"] = /api/variants/{variantId}/pages GET / registered on that group (ImageEndpoints.cs:24,27)

All call sites were updated for the new signatures (VariantEndpoints.cs:113-114,126-127; HypermediaHelpersTests.cs:147). No remaining callers of the old signatures. Tests updated to assert the corrected hrefs and added Assert.DoesNotContain("titles", ...) as a regression guard — good.

No new blocking issues found in the fix delta.

Verification

  • CI/CD: Forgejo Actions coverage report present and updated for head 37ef8354 (generated 06/28 17:49, 4 assemblies, line 82.7% / branch 45.2%, HypermediaHelpers 86.1% line / 100% branch). Build+test ran successfully for this head; local build/test skipped per CI-evidence policy.
  • Correctness findings above are from static cross-reference of the fix diff against route registrations.

Note: this is a PR conversation comment — not a formal Forgejo review approval (the MCP integration cannot create approval states or inline diff comments).


Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: no blocking issues found (fixes verified) Follow-up review of head `37ef8354` (the `fix: correct all broken hypermedia hrefs` commit). I statically verified each of the three major + one minor finding from my previous review against the actual `MapGroup(...)` / `MapVerb(...)` route registrations. ### ✅ All previously-reported issues resolved | # | Finding | Fix in `37ef835` | Route verified? | |---|---------|------------------|-----------------| | 1 | Dead `titles` link in `BuildDoujinLinks` | `["titles"]` removed (comment explains why) | ✅ No `GET /api/doujins/{id}/titles` route exists (only POST/DELETE) — removal is correct | | 2 | Chapter `self` → nonexistent `/api/chapters/{id}` | Changed to `["collection"] = /api/variants/{variantId}/chapters` | ✅ `GET /` registered on that group (`VariantEndpoints.cs:87,90`) | | 3 | Chapter `update`/`delete` missing `variants/{variantId}` prefix | `BuildChapterActions` now takes `variantId`; emits nested paths | ✅ `PUT/DELETE /{chapterId}` registered under `/api/variants/{variantId:guid}/chapters` (`VariantEndpoints.cs:118,131`) | | 4 (minor) | `BuildPageLinks` `self` → nonexistent `/api/pages/{id}` | Changed to `["collection"] = /api/variants/{variantId}/pages` | ✅ `GET /` registered on that group (`ImageEndpoints.cs:24,27`) | All call sites were updated for the new signatures (`VariantEndpoints.cs:113-114,126-127`; `HypermediaHelpersTests.cs:147`). No remaining callers of the old signatures. Tests updated to assert the corrected hrefs and added `Assert.DoesNotContain("titles", ...)` as a regression guard — good. No new blocking issues found in the fix delta. ### ✅ Verification - **CI/CD:** Forgejo Actions coverage report present and updated for head `37ef8354` (generated 06/28 17:49, 4 assemblies, line 82.7% / branch 45.2%, `HypermediaHelpers` 86.1% line / 100% branch). Build+test ran successfully for this head; **local build/test skipped** per CI-evidence policy. - Correctness findings above are from static cross-reference of the fix diff against route registrations. *Note: this is a PR conversation comment — not a formal Forgejo review approval (the MCP integration cannot create approval states or inline diff comments).* --- *Automated daily review. I never merge PRs.*
bjoern merged commit be0eb0b3f7 into main 2026-06-28 20:17:40 +02:00
bjoern deleted branch feat/hypermedia-responses 2026-06-28 20:17:40 +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!8
No description provided.