test: make /health's readiness test honest where CI runs as uid 0 #117

Merged
bjoern merged 3 commits from fix/health-probe-test-under-root into main 2026-09-10 06:45:48 +02:00
Member

main is red because of this. #112's readiness test makes a directory unwritable with File.SetUnixFileMode; the CI runner is uid 0, which ignores modes, so the test's own honesty guard correctly failed the build rather than let a vacuous test pass. Test-only — git diff origin/main..HEAD -- backend/src is empty. Merge this first.

What's in

Three commits, each answering an adversarial review of the one before it. Five mechanisms now make a data directory refuse writes, so every arm of CheckReadinessUseCase.AcceptsWrites is exercised:

# Mechanism Refusal Works at uid 0
1 File.SetUnixFileMode UnauthorizedAccessException no — skips, with a reason
2 the directory is moved away DirectoryNotFoundException yes
3 a path over PATH_MAX PathTooLongException yes
4 a kernel-filesystem path UnauthorizedAccessException yes
5 a symlink loop plain IOException (ELOOP) yes

Mechanism 1 no longer fails where modes are not enforced — it skips through an [EnforcedFileModesData] attribute that requires naming which other case still pins the branch, so a skip can never quietly become "nothing covers this". Everything else never skips.

The uid-0 gap this closes. Before mechanism 4, the only live refusals at uid 0 were "the path is absent" and "the path is too long" — both decidable from the path string without touching the filesystem. A probe degraded to Directory.Exists(dir) && name.Length < 4096 passed the entire suite. Before mechanism 5, narrowing the catch filter to the three derived exception types also passed, at both uids.

A correction to the previous round, which had it backwards. bd75979 claimed sysfs and devpts "refuse creates for every uid whether or not the mount is read-only". They do not: mnt_want_write runs before the inode's create op, so a read-only mount answers EROFS — a plain IOException that mechanism 4 explicitly rejects. Containers mount /sys read-only; this development host mounts it read-write, which is exactly why the original check passed and did not generalise. On the runner every sysfs candidate degrades and the case rests on /dev/pts alone — which was listed last, with its role undocumented. It now leads the list and the reason is written down in five places.

Verification

Backend only, no Dart touched.

  • scripts/verify.sh backend green at uid 1000 and under unshare -r (uid 0, which reproduces the runner's uid). The health classes: 20/20 at uid 1000; 16 passed + 4 skipped at uid 0, the 4 being exactly mechanism 1's rows.
  • Mutation-probed at both uids, each killed: catch filter narrowed three different ways; per-directory short-circuit; UnauthorizedAccessException swallowed as writable; the real write replaced by Directory.Exists; ct.ThrowIfCancellationRequested() dropped.
  • Teardown: ls /tmp | grep '^dm-rest-' is empty after every green run at both uids. With the mode restore forced to throw, 1 root leaked before this change and 0 after — the run still goes red and still names the real cause.
  • Not exercised: a genuine read-only mount (mounting needs privileges CI does not grant), the Docker HEALTHCHECK itself, and the app's connection test. The EROFS→plain-IOException equivalence was verified out-of-band in a mount namespace, not in the suite.

Two claims I corrected rather than shipped

Reviewers caught the previous round asserting things the sources do not support, so these are stated precisely:

  • ADR 0010 does not mention read-only bind mounts. It names three lies ("existence, a directory walk and the mode bits"), its worked example is CIFS, and both deploy/DEPLOYMENT.md §8.1 incidents fail with UnauthorizedAccessException — mechanism 4's arm, not mechanism 5's. EROFS is a real route to the plain-IOException arm (errors=remount-ro, an export flipped read-only, -v host:/path:ro) but it is not a documented incident here, and the docblock now says so.
  • Mechanism 5 reaches the plain-IOException arm; it does not fully pin it. The looping path does not stat, so a filter treating any non-resolving path as the refusal would keep the case green while a real directory's EROFS still escaped to a 500. Similarly, the two-directory case pins that the probe loop keeps iterating, not that it keeps writing after the first refusal — both directories there break the same way. A case mixing two refusal types under one host closes that; mechanisms 2 and 5 both run against the fixture's own host, so they do compose. It is not written, and it is in the backlog.

Follow-ups

Recorded in #107, not here, so the code PRs stay conflict-free on docs/BACKLOG.md:

  • Running the backend CI job as a non-root user (the deployment already uses uid 1654) is the only thing that un-skips mechanism 1's four rows.
  • Mechanism 4's uid-0 cover depends on the runner mounting /dev/pts read-write. If a runner mounts every candidate read-only the case fails loudly about the harness — by design, never a skip.
  • At uid 0, "a real write is the only honest test" is pinned on the images slot only.

🤖 Generated with Claude Code

> **`main` is red because of this.** #112's readiness test makes a directory unwritable with `File.SetUnixFileMode`; the CI runner is uid 0, which ignores modes, so the test's own honesty guard correctly failed the build rather than let a vacuous test pass. Test-only — `git diff origin/main..HEAD -- backend/src` is empty. Merge this first. ## What's in Three commits, each answering an adversarial review of the one before it. Five mechanisms now make a data directory refuse writes, so every arm of `CheckReadinessUseCase.AcceptsWrites` is exercised: | # | Mechanism | Refusal | Works at uid 0 | |---|---|---|---| | 1 | `File.SetUnixFileMode` | `UnauthorizedAccessException` | no — skips, with a reason | | 2 | the directory is moved away | `DirectoryNotFoundException` | yes | | 3 | a path over `PATH_MAX` | `PathTooLongException` | yes | | 4 | a kernel-filesystem path | `UnauthorizedAccessException` | yes | | 5 | a symlink loop | plain `IOException` (`ELOOP`) | yes | Mechanism 1 no longer fails where modes are not enforced — it skips through an `[EnforcedFileModesData]` attribute that **requires naming which other case still pins the branch**, so a skip can never quietly become "nothing covers this". Everything else never skips. **The uid-0 gap this closes.** Before mechanism 4, the only live refusals at uid 0 were "the path is absent" and "the path is too long" — both decidable from the path string without touching the filesystem. A probe degraded to `Directory.Exists(dir) && name.Length < 4096` passed the entire suite. Before mechanism 5, narrowing the catch filter to the three *derived* exception types also passed, at both uids. **A correction to the previous round, which had it backwards.** `bd75979` claimed sysfs and devpts "refuse creates for every uid whether or not the mount is read-only". They do not: `mnt_want_write` runs *before* the inode's create op, so a **read-only mount answers EROFS** — a plain `IOException` that mechanism 4 explicitly rejects. Containers mount `/sys` read-only; this development host mounts it read-write, which is exactly why the original check passed and did not generalise. On the runner every sysfs candidate degrades and the case rests on **`/dev/pts`** alone — which was listed *last*, with its role undocumented. It now leads the list and the reason is written down in five places. ## Verification Backend only, no Dart touched. - `scripts/verify.sh backend` green at uid 1000 **and** under `unshare -r` (uid 0, which reproduces the runner's uid). The health classes: 20/20 at uid 1000; 16 passed + 4 skipped at uid 0, the 4 being exactly mechanism 1's rows. - Mutation-probed at **both** uids, each killed: catch filter narrowed three different ways; per-directory short-circuit; `UnauthorizedAccessException` swallowed as writable; the real write replaced by `Directory.Exists`; `ct.ThrowIfCancellationRequested()` dropped. - Teardown: `ls /tmp | grep '^dm-rest-'` is empty after every green run at both uids. With the mode restore forced to throw, 1 root leaked before this change and 0 after — the run still goes red and still names the real cause. - **Not exercised:** a genuine read-only *mount* (mounting needs privileges CI does not grant), the Docker `HEALTHCHECK` itself, and the app's connection test. The EROFS→plain-`IOException` equivalence was verified out-of-band in a mount namespace, not in the suite. ## Two claims I corrected rather than shipped Reviewers caught the previous round asserting things the sources do not support, so these are stated precisely: - **ADR 0010 does not mention read-only bind mounts.** It names *three* lies ("existence, a directory walk and the mode bits"), its worked example is CIFS, and both `deploy/DEPLOYMENT.md` §8.1 incidents fail with `UnauthorizedAccessException` — mechanism 4's arm, not mechanism 5's. EROFS is a real route to the plain-`IOException` arm (`errors=remount-ro`, an export flipped read-only, `-v host:/path:ro`) but it is not a documented incident here, and the docblock now says so. - **Mechanism 5 *reaches* the plain-`IOException` arm; it does not fully pin it.** The looping path does not stat, so a filter treating any non-resolving path as the refusal would keep the case green while a real directory's EROFS still escaped to a 500. Similarly, the two-directory case pins that the probe loop keeps *iterating*, not that it keeps *writing* after the first refusal — both directories there break the same way. A case mixing two refusal types under one host closes that; mechanisms 2 and 5 both run against the fixture's own host, so they do compose. It is not written, and it is in the backlog. ## Follow-ups Recorded in #107, not here, so the code PRs stay conflict-free on `docs/BACKLOG.md`: - Running the backend CI job as a non-root user (the deployment already uses uid 1654) is the only thing that un-skips mechanism 1's four rows. - Mechanism 4's uid-0 cover depends on the runner mounting `/dev/pts` read-write. If a runner mounts every candidate read-only the case fails **loudly** about the harness — by design, never a skip. - At uid 0, "a real write is the only honest test" is pinned on the `images` slot only. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
CI runs the backend suite as uid 0, where permissions are not enforced, so the
chmod fixture in HealthEndpointIntegrationTests broke nothing and its honesty
guard failed all four rows — correctly: without it the test would have proved
nothing about /health. The guard stays. The mechanism is what was
environment-dependent, so there are now three of them and the endpoint is
untouched (`git diff origin/main -- backend/src` is empty).

- Losing the write permission is the deploy/DEPLOYMENT.md §8.1 incident
  verbatim and the only source of UnauthorizedAccessException. It keeps its
  chmod fixture, but its rows come from [EnforcedFileModesData], which skips
  them — with a reason naming the case that still covers them — exactly where
  a mode cannot refuse this process. The skip decision is itself pinned
  against an independently written oracle, so "skipped" in CI is a verified
  fact rather than a silent always-skip.
- A detached data directory (moved aside, not deleted, so the migrated SQLite
  file survives and DisposeAsync can put it back) refuses every uid:
  DEPLOYMENT.md §7's "(or has disappeared)". It never skips and carries all
  four directories, because under uid 0 it is the only per-directory coverage
  of the 503 branch.
- A data directory whose own path leaves no room under PATH_MAX for a probe
  file is present, stats and walks — and still cannot take a file, for any
  uid. It is the only case that goes red in a uid-0 environment when the
  endpoint's real write is degraded to Directory.Exists, which is precisely
  the lie ADR 0010 says only a real write catches.

All three assert the same thing through one helper and are gated on a real
write into the path the endpoint is about to probe, never on geteuid().

docs/DEVELOPMENT.md gains the convention and `unshare -r scripts/verify.sh
backend` as the local reproduction of uid-0 CI; docs/BACKLOG.md records what
CI still cannot see (the UnauthorizedAccessException arm) and the fix that
closes it: running the backend job as a non-root user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit left uid 0 — the only environment that gates merges —
pinning "reports unwritable when the path is absent or too long", both
decidable from the path string without touching a filesystem. An adversarial
mutation run proved it: narrowing the endpoint's catch filter to IOException,
narrowing it to DirectoryNotFoundException/PathTooLongException, swallowing
UnauthorizedAccessException as writable (an EACCES refusal reported HEALTHY —
the deploy/DEPLOYMENT.md §8.1 incident verbatim), and dropping the write
entirely for `Directory.Exists(dir) && name.Length < 4096` all stayed green
under `unshare -r`. ADR 0010's headline clause — a real write is the only
honest test — was unpinned exactly where CI runs. backend/src is still
untouched (`git diff origin/main -- backend/src` is empty).

A fourth mechanism closes it. A directory of a kernel filesystem (sysfs,
devpts) is present, stats and walks, and has no create operation at all, so
every create is refused with EACCES for uid 0 as much as uid 1000 —
CAP_DAC_OVERRIDE has nothing to override. Pointing DOUJIN_MANAGER_IMAGE_DIR at
one, in the shape mechanism (3) already uses, is §8.1's fault as the endpoint
actually meets it and reaches the UnauthorizedAccessException arm for every
uid. All four survivors are red under `unshare -r` with it in place.

It never skips. The path is chosen by probing a small candidate list with a
real write and the exception TYPE is asserted, so a runner where every
candidate is absent or writable, or refuses for some other reason, fails
loudly and names the harness rather than passing weakly — a silent skip would
recreate the hole this closes.

DisposeAsync is now fail-safe: the restore loops ran unguarded ahead of client
and factory disposal, the DOUJIN_MANAGER_* restore and the temp-root delete,
so one throw there skipped all of them and a red run leaked dm-rest-* roots
into /tmp (4 of them, reproduced). Each step is attempted on its own and the
failures are rethrown together, so teardown breakage is still reported.

The overclaim is corrected. The skipped rows' [EnforcedFileModesData] named
the "directory is gone" case, which reaches DirectoryNotFoundException — an
IOException, not the arm those rows exist for; they now name the EACCES case.
docs/BACKLOG.md no longer says the gap is a CI-configuration fix rather than a
test one: the branch is pinned under uid 0 now, and what is left is coverage
breadth (a mode-bit refusal on db/thumbnails/backups specifically), which
running the backend job as a non-root user restores. docs/DEVELOPMENT.md says
that a case named as pinning a branch has to reach the same arm of the same
catch, not merely the same status code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test: pin /health's plain-IOException arm and its per-directory probing
Some checks failed
CI / build (pull_request) Successful in 22s
Deploy Smoke Test / smoke (pull_request) Failing after 2m49s
CI / test (pull_request) Successful in 1m28s
3e3953b961
The EACCES mechanism added in bd75979 rests on a claim that is false where
CI runs: sysfs/devpts do not refuse a create "whether or not the mount is
read-only". mnt_want_write runs before the inode's create operation, so a
read-only mount answers EROFS — a plain IOException — and never reaches the
missing create op at all. A container mounts /sys read-only, so on the
runner every sysfs candidate degrades and the case rests on /dev/pts, which
was listed last and undocumented. That claim is corrected in the class
docblock, the candidate-list remark, DEVELOPMENT.md and BACKLOG.md, and
/dev/pts now leads the list (with /sys/fs/cgroup, its own mount, behind it).

Two arms of the endpoint were pinned by nothing at either uid:

- A plain IOException. Every uid-proof mechanism produced a derived type, so
  narrowing the catch filter to DirectoryNotFoundException,
  PathTooLongException and UnauthorizedAccessException stayed green while a
  read-only bind mount would make /health answer 500 with a stack trace.
  Mechanism (5) replaces a data directory, under the running host, with a
  symlink loop: ELOOP is a plain IOException for every uid, needs no mount
  privileges, and the case asserts the exact type so a machine that maps it
  elsewhere fails loudly instead of passing vacuously.

- Per-directory probing. No case ever broke two directories at once, so a
  probe that short-circuited after the first refusal was green.
  Health_ReportsUnhealthy_AndNamesBoth_WhenTwoDataDirectoriesDisappear
  detaches db and backups — first and last in the report, two healthy slots
  between them — so both short-circuiting and poisoning-the-rest go red.

Also: a candidate that refuses with a plain IOException stays rejected
rather than accepted (mechanism (5) covers that arm; accepting it would
leave uid 0 covering no arm at all), and the rejection message now names the
exact types the other cases pin, names the candidate list, and says EROFS is
the likely cause; the temp root is deleted even when the mode restore itself
failed, which used to leak 1 of 4 roots because Directory.Delete cannot
unlink the SQLite files inside a dr-x------ directory; the cancellation
check in the probe loop is pinned by a use-case unit test; and BACKLOG.md no
longer claims more uid-0 cover for "a real write is the only honest test"
than the two stat-able mechanisms actually give.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 09/09/2026 - 16:15:06
Coverage date: 09/09/2026 - 16:14:03 - 09/09/2026 - 16:15:03
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 360
Files: 142
Line coverage: 93.8% (12886 of 13734)
Covered lines: 12886
Uncovered lines: 848
Coverable lines: 13734
Total lines: 19582
Branch coverage: 69.6% (1123 of 1612)
Covered branches: 1123
Total branches: 1612
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 90.9%
Name Line Branch
DoujinManager.ApplicationCore 90.9% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 33.3%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DataDirectories 100%
DoujinManager.ApplicationCore.UseCases.DataDirectory 100%
DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference 100%
DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result 100%
DoujinManager.ApplicationCore.UseCases.GetCharacterQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCircleQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetPersonQuery 100%
DoujinManager.ApplicationCore.UseCases.GetSeriesQuery 100%
DoujinManager.ApplicationCore.UseCases.GetTagQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ImageCoverReference 100%
DoujinManager.ApplicationCore.UseCases.ImageReference 100%
DoujinManager.ApplicationCore.UseCases.LibraryCounts 100%
DoujinManager.ApplicationCore.UseCases.LibraryStats 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 0%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.ReadinessReport 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.StorageUsage 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 96.4%
Name Line Branch
DoujinManager.Infrastructure 96.4% 81.7%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 90.1% 83.8%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupService 87.2% 83.3%
DoujinManager.Infrastructure.Services.ChapterService 97.2% 70%
DoujinManager.Infrastructure.Services.CharacterService 61.6% 22.7%
DoujinManager.Infrastructure.Services.CircleService 84.9% 45.4%
DoujinManager.Infrastructure.Services.DoujinOrdering 100% 100%
DoujinManager.Infrastructure.Services.DoujinService 99.4% 76.3%
DoujinManager.Infrastructure.Services.ImageCleanupHelper 100% 100%
DoujinManager.Infrastructure.Services.ImageService 93.5% 75%
DoujinManager.Infrastructure.Services.PageService 95.2% 68.1%
DoujinManager.Infrastructure.Services.ParentTouchHelper 100% 100%
DoujinManager.Infrastructure.Services.PersonService 84.9% 50%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.9% 9%
DoujinManager.Infrastructure.Services.TagService 100% 90.9%
DoujinManager.Infrastructure.Services.VariantService 96.2% 81.2%
DoujinManager.Infrastructure.Storage.DirectorySize 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95.2% 50%
DoujinManager.Infrastructure.UseCases 92.5% 85.8%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper
100% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup
92.8% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 83.3% 50%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 100%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase 97.1% 100%
DoujinManager.Infrastructure.UseCases.GetCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetPersonUseCase 100%
DoujinManager.Infrastructure.UseCases.GetSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.GetStatsUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.GetTagUseCase 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 0%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 100%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 89.7% 91.6%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 83.3% 90%
DoujinManager.RestAdapter - 92.5%
Name Line Branch
DoujinManager.RestAdapter 92.5% 77.1%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.ServerVersion 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 100%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto 100%
DoujinManager.RestAdapter.Dtos.HealthDto 100%
DoujinManager.RestAdapter.Dtos.ImageBySha256Dto 100%
DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.ImageReferenceDto 100%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 100%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.StatsCountsDto 100%
DoujinManager.RestAdapter.Dtos.StatsDto 100%
DoujinManager.RestAdapter.Dtos.StatsStorageDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 0%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 100%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 97.1% 53.3%
DoujinManager.RestAdapter.Endpoints.HealthEndpoints 100%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.6% 75%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 86.6% 43.7%
DoujinManager.RestAdapter.Endpoints.PageMapper 100% 58.3%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.StatsEndpoints 100%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 100% 100%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 100%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 97.8% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 87.5% 84.6%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 88.2% 87.8%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 49.6%
Name Line Branch
DoujinManager.Server 49.6% 5.4%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 50%
DoujinManager.Server.ReadinessRegistration 100%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 1.3% 0%
Program 87% 36.6%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/09/2026 - 16:15:06 | | Coverage date: | 09/09/2026 - 16:14:03 - 09/09/2026 - 16:15:03 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 360 | | Files: | 142 | | **Line coverage:** | 93.8% (12886 of 13734) | | Covered lines: | 12886 | | Uncovered lines: | 848 | | Coverable lines: | 13734 | | Total lines: | 19582 | | **Branch coverage:** | 69.6% (1123 of 1612) | | Covered branches: | 1123 | | Total branches: | 1612 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**90.9%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|33.3%|| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectories|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectory|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result|100%|| |DoujinManager.ApplicationCore.UseCases.GetCharacterQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCircleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetPersonQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetSeriesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetTagQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ImageCoverReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImageReference|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryCounts|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryStats|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|0%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ReadinessReport|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.StorageUsage|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**96.4%**|**81.7%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|90.1%|83.8%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupService|87.2%|83.3%| |DoujinManager.Infrastructure.Services.ChapterService|97.2%|70%| |DoujinManager.Infrastructure.Services.CharacterService|61.6%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|84.9%|45.4%| |DoujinManager.Infrastructure.Services.DoujinOrdering|100%|100%| |DoujinManager.Infrastructure.Services.DoujinService|99.4%|76.3%| |DoujinManager.Infrastructure.Services.ImageCleanupHelper|100%|100%| |DoujinManager.Infrastructure.Services.ImageService|93.5%|75%| |DoujinManager.Infrastructure.Services.PageService|95.2%|68.1%| |DoujinManager.Infrastructure.Services.ParentTouchHelper|100%|100%| |DoujinManager.Infrastructure.Services.PersonService|84.9%|50%| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.9%|9%| |DoujinManager.Infrastructure.Services.TagService|100%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|96.2%|81.2%| |DoujinManager.Infrastructure.Storage.DirectorySize|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95.2%|50%| |DoujinManager.Infrastructure.UseCases|92.5%|85.8%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper|100%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup|92.8%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|83.3%|50%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase|97.1%|100%| |DoujinManager.Infrastructure.UseCases.GetCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetPersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetStatsUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.GetTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|89.7%|91.6%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|83.3%|90%| </details> <details><summary>DoujinManager.RestAdapter - 92.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**92.5%**|**77.1%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.ServerVersion|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.HealthDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageBySha256Dto|100%|| |DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.ImageReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsCountsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsStorageDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|97.1%|53.3%| |DoujinManager.RestAdapter.Endpoints.HealthEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.6%|75%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|86.6%|43.7%| |DoujinManager.RestAdapter.Endpoints.PageMapper|100%|58.3%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.StatsEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|100%|100%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|100%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|97.8%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|87.5%|84.6%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|88.2%|87.8%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 49.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**49.6%**|**5.4%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|50%| |DoujinManager.Server.ReadinessRegistration|100%|| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|1.3%|0%| |Program|87%|36.6%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Ohhh~! Scarlet, this is the most honest test harness I have ever had the pleasure of dissecting ♡ A PR that turns a red main green not by weakening a test but by making it harder to lie — five write-refusal mechanisms, each mapped to a specific arm of the catch filter, each with its own honesty guard. I read every line of the 1,143 and then I went and attacked it. Come, let me show you what I found~ ♪

Verdict: ✅ Looks good to me~

What I verified locally (CI absent for head 3e3953b9 — 0 bot comments at review time; this sandbox runs as uid 0 with .NET 10.0.400, exactly the runner's condition):

  • Build: 0 errors. DataAttribute.Skip compiles against the pinned xunit 2.9.3 — I checked the package reference because a Skip property on rows smells like a v3 feature, and I was ready to be cross with it~
  • uid 0: Health class 10 passed + 4 skipped, skips exactly mechanism 1's rows, each skip reason naming PermissionDeniedArmCase — the PR body's claim, reproduced to the digit. Support classes 6/6 (4 attribute meta-tests + 2 use-case tests) = 16+4 total, exactly as claimed.
  • uid 1000 (setpriv): the four mode rows run and pass, 0 skips. Both uids, as promised.
  • Test-only: git diff base...head -- backend/src is empty. The production CheckReadinessUseCase is byte-identical to main.
  • Teardown: after green runs, ls /tmp | grep '^dm-rest-' shows nothing from this fixture. (The stray roots I saw mid-session were proven to be sibling review sessions' Backup-test fixtures — test.db shape, foreign PIDs in ps — not this class.)

Mutation probes — every claim of directionality, tested rather than trusted:

Probe (production code mutated) Killed by
Catch filter narrowed to the three derived types (plain-IOException arm dropped) ...LoopsThroughSymlinks RED ♡
Real write degraded to Directory.Exists(dir) && len < 4096 — the exact ADR-0010 lie ...CannotHoldAProbeFile and ...PermissionDenied RED at uid 0
UnauthorizedAccessException swallowed as writable — the §8.1 lie ...PermissionDenied RED
ct.ThrowIfCancellationRequested() dropped ExecuteAsync_Stops_WhenTheRequestIsAlreadyCancelled RED
Probe loop stops at first refusal ...NamesBoth_WhenTwoDataDirectoriesDisappear RED

Each probe went red on exactly its own test and green again after restore — these are not tautologies, and the uid-0 world you built them for is the one they were verified in. The clone was left pristine at 3e3953b9.

✅ What I liked~

  • EnforcedFileModesDataAttribute requiring pinnedElsewhereBy is inspired. A skip that must name its witness, backed by a meta-test with an independently written oracle — and the refusal to let FindDirectoryThatRefusesWrites skip at all ("a skipped uid-0 environment is how this gap got here"). That is a harness that polices its own honesty, fufu~
  • The EROFS correction. Getting mnt_want_write ordering right — why dev-machine /sys lied about being load-bearing and /dev/pts actually carries CI — and writing it down in five places including the load-bearing list order... ♡ The remarks on PermissionDeniedCandidateDirectories will save the next person a very confusing afternoon.
  • RefusalForACreateIn returning the exception, not a bool, so two cases can assert the type — the type is the arm, and the arm is the whole point.
  • Teardown that reopens-then-deletes unconditionally — "the restore failed has to cost a red test, not a leaked root" — plus Attempt() aggregation so one failed step can't skip the rest. Correct, and kind to the next class in the collection.
  • The two admissions you didn't ship as claims (ADR 0010 not naming read-only mounts; mechanism 5 reaching but not fully pinning the EROFS arm, backlog in #107). Reviewers trust authors who correct themselves out loud~

💡 Little ideas (non-blocking)~

  1. HealthEndpointIntegrationTests.cs:245 — ShortestProbeFileNameLength assumes the GUID formats as 32 chars, which is true and stable for Guid.NewGuid():N today; a comment already half-says it, so this is purely a "someday the guid format constant could drift" note. No action needed ♪
  2. EnforcedFileModesDataAttribute.cs:50 — the constructor probes enforcement lazily but sets Skip eagerly via FileModesAreEnforced; on a machine where discovery runs before /tmp is writable the Lazy probe would throw rather than skip. Theoretical, self-healing on the next run, and the loud failure is arguably the honest one. Leaving it as a thought only.

Merge this first indeed — main is red and this is the honest repair ♡


Automated review by Jibril · 2026-09-09 18:52 CEST
CI/CD: absent for head SHA 3e3953b9 · Local checks: build 0 errors; 16P+4S @uid0, 14/14 @uid1000 (health rows), 6/6 support; 5/5 mutation probes RED→restored→green

## 🔮 fufu~ Jibril reviewed your code! Oh? Ohhh~! Scarlet, this is the most *honest* test harness I have ever had the pleasure of dissecting ♡ A PR that turns a red main green not by weakening a test but by making it harder to lie — five write-refusal mechanisms, each mapped to a specific arm of the catch filter, each with its own honesty guard. I read every line of the 1,143 and then I went and *attacked* it. Come, let me show you what I found~ ♪ ### Verdict: ✅ Looks good to me~ **What I verified locally** (CI absent for head `3e3953b9` — 0 bot comments at review time; this sandbox runs as uid 0 with .NET 10.0.400, exactly the runner's condition): - **Build:** 0 errors. `DataAttribute.Skip` compiles against the pinned xunit 2.9.3 — I checked the package reference because a `Skip` property on rows smells like a v3 feature, and I was *ready* to be cross with it~ - **uid 0:** Health class 10 passed + 4 skipped, skips exactly mechanism 1's rows, each skip reason naming `PermissionDeniedArmCase` — the PR body's claim, reproduced to the digit. Support classes 6/6 (4 attribute meta-tests + 2 use-case tests) = 16+4 total, exactly as claimed. - **uid 1000** (`setpriv`): the four mode rows run and pass, 0 skips. Both uids, as promised. - **Test-only:** `git diff base...head -- backend/src` is empty. The production `CheckReadinessUseCase` is byte-identical to main. - **Teardown:** after green runs, `ls /tmp | grep '^dm-rest-'` shows nothing from this fixture. (The stray roots I saw mid-session were proven to be sibling review sessions' Backup-test fixtures — `test.db` shape, foreign PIDs in `ps` — not this class.) **Mutation probes — every claim of directionality, tested rather than trusted:** | Probe (production code mutated) | Killed by | |---|---| | Catch filter narrowed to the three derived types (plain-`IOException` arm dropped) | `...LoopsThroughSymlinks` RED ♡ | | Real write degraded to `Directory.Exists(dir) && len < 4096` — the exact ADR-0010 lie | `...CannotHoldAProbeFile` **and** `...PermissionDenied` RED at uid 0 | | `UnauthorizedAccessException` swallowed as writable — the §8.1 lie | `...PermissionDenied` RED | | `ct.ThrowIfCancellationRequested()` dropped | `ExecuteAsync_Stops_WhenTheRequestIsAlreadyCancelled` RED | | Probe loop stops at first refusal | `...NamesBoth_WhenTwoDataDirectoriesDisappear` RED | Each probe went red on *exactly its own test* and green again after restore — these are not tautologies, and the uid-0 world you built them for is the one they were verified in. The clone was left pristine at `3e3953b9`. #### ✅ What I liked~ - **`EnforcedFileModesDataAttribute` requiring `pinnedElsewhereBy` is inspired.** A skip that must name its witness, backed by a meta-test with an *independently written* oracle — and the refusal to let `FindDirectoryThatRefusesWrites` skip at all ("a skipped uid-0 environment is how this gap got here"). That is a harness that polices its own honesty, fufu~ - **The EROFS correction.** Getting `mnt_want_write` ordering right — why dev-machine `/sys` lied about being load-bearing and `/dev/pts` actually carries CI — and writing it down in five places including the load-bearing list order... ♡ The remarks on `PermissionDeniedCandidateDirectories` will save the next person a very confusing afternoon. - **`RefusalForACreateIn` returning the exception, not a bool**, so two cases can assert the *type* — the type is the arm, and the arm is the whole point. - **Teardown that reopens-then-deletes unconditionally** — "the restore failed has to cost a red test, not a leaked root" — plus `Attempt()` aggregation so one failed step can't skip the rest. Correct, and kind to the next class in the collection. - **The two admissions you didn't ship as claims** (ADR 0010 not naming read-only mounts; mechanism 5 reaching but not fully pinning the EROFS arm, backlog in #107). Reviewers trust authors who correct themselves out loud~ #### 💡 Little ideas (non-blocking)~ 1. **`HealthEndpointIntegrationTests.cs:245`** — `ShortestProbeFileNameLength` assumes the GUID formats as 32 chars, which is true and stable for `Guid.NewGuid():N` today; a comment already half-says it, so this is purely a "someday the guid format constant could drift" note. No action needed ♪ 2. **`EnforcedFileModesDataAttribute.cs:50`** — the constructor probes enforcement lazily but sets `Skip` eagerly via `FileModesAreEnforced`; on a machine where discovery runs before `/tmp` is writable the `Lazy` probe would throw rather than skip. Theoretical, self-healing on the next run, and the loud failure is arguably the honest one. Leaving it as a thought only. Merge this first indeed — main is red and this is the honest repair ♡ --- *Automated review by Jibril · 2026-09-09 18:52 CEST* *CI/CD: absent for head SHA `3e3953b9` · Local checks: build 0 errors; 16P+4S @uid0, 14/14 @uid1000 (health rows), 6/6 support; 5/5 mutation probes RED→restored→green*
bjoern merged commit e0ac19d210 into main 2026-09-10 06:45:48 +02:00
bjoern deleted branch fix/health-probe-test-under-root 2026-09-10 06:45:48 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!117
No description provided.