feat(server): add Audiobookshelf and Calibre-Web Automated migration sources

Extend the migration module beyond Booklore and Grimmory so an existing Audiobookshelf or
Calibre-Web Automated library can be moved into BookOrbit without rebuilding user state by
hand. Audiobookshelf supports both a live API connection and an offline backup file, while
Calibre-Web Automated reads a stopped snapshot of the app and metadata databases.

Match source books by ASIN in addition to ISBN, and batch the ISBN, ASIN, file hash, file
path, and title/author lookups so planning a large library issues a bounded number of
queries rather than one per book. Route the batched author comparison through the shared
accent-insensitive predicate, which now accepts a SQL fragment so a per-row pattern column
is matched the same way a bound literal is.

Extract the shared source connection form into MigrationSourceFields so the modal and the
settings page stay in sync as source types are added, and allow a mapping to skip a source
user instead of forcing every account to be imported.

Closes #986
This commit is contained in:
neon
2026-08-16 05:15:34 +00:00
parent 5527852b4b
commit ec4072e4be
87 changed files with 13988 additions and 698 deletions
+218
View File
@@ -0,0 +1,218 @@
# Migrating from Audiobookshelf
BookOrbit can perform a one-time import of reading state from Audiobookshelf. The migration matches
Audiobookshelf books to books that already exist in BookOrbit, maps Audiobookshelf users to existing
BookOrbit users, and imports the selected users' supported state.
This is an import, not a continuous synchronization. Keep both systems backed up until you have
reviewed the migration report and verified the imported state.
## Compatibility
Compatibility is declared only for versions covered by the disposable end-to-end suite.
| Mode | Verified Audiobookshelf version | Verification |
| -------- | ------------------------------- | ------------------------------------------------------------ |
| Live API | 2.36.0 | Full import, repeat-run idempotency, and database integrity |
| Backup | Backup produced by 2.36.0 | Live/backup normalization parity and dry-run planning parity |
Other Audiobookshelf versions may work, but they are not currently declared compatible. A missing
optional backup table produces a warning and omits that domain. A missing required table or column
causes validation to fail instead of importing incomplete or misidentified state.
Maintainers can exercise another image with:
```bash
ABS_E2E_IMAGE=ghcr.io/advplyr/audiobookshelf:<version> pnpm e2e:run -- migration-audiobookshelf
```
Do not broaden the compatibility declaration until both live API behavior and a backup produced by
that version pass the suite.
## Requirements
- The BookOrbit operator must have the `Manage App Settings` permission.
- Live API mode requires an Audiobookshelf access token for an `admin` or `root` user. Tokens for
regular and guest users are rejected.
- Target books must already exist in a scanned BookOrbit library. Unmatched source books are reported
and are not created automatically.
- Target BookOrbit user accounts must already exist. Audiobookshelf users are mapping candidates only;
accounts, passwords, roles, and permissions are not copied.
- Configure `MIGRATION_ENCRYPTION_KEY` with a persistent 64-character hexadecimal key before saving a
live API source. Without a valid key, saved source credentials are not encrypted at rest. Generate a
key with `openssl rand -hex 32` and retain it for as long as the saved source is needed.
## Supported data
The first compatibility declaration covers:
- book identity and metadata used by the migration, including title, subtitle, authors, narrators,
first series membership, ISBN, ASIN, publisher, year, language, description, genres, and tags;
- explicit user mapping to existing BookOrbit accounts;
- read, reading, and unread status from real Audiobookshelf progress records, including available start,
finish, and update dates;
- multi-file audiobook position, resolved to the correct target audio file and file-local second;
- EPUB percentage and EPUB CFI position;
- separate audio and EPUB positions for mixed-format books;
- audio bookmarks with their absolute book position, title, and creation time;
- valid listening sessions; and
- matching by ISBN, ASIN, mapped file path, then title and author.
The migration reports unmatched books, ambiguous matches, malformed source records, and state owned by
unmapped users. It does not assign that state to another user.
Metadata is applied as an overlay per field. A field Audiobookshelf actually carries replaces the value
on the matched BookOrbit book; a field Audiobookshelf leaves empty is skipped rather than written as
empty. An Audiobookshelf book with no publisher therefore leaves an existing BookOrbit publisher intact.
An explicit value is still an overlay: `abridged: false` and a zero duration are written.
Author sort names come from the `Last, First` form Audiobookshelf stores. A backup carries that per
author. The live API only exposes one joined sort string per book, so it is applied when the book has
exactly one author, and other books fall back to sorting on the display name.
## Deferred and unavailable data
The following data is not imported:
- Audiobookshelf podcasts and podcast sessions;
- user account creation, credentials, roles, or permissions;
- annotations, because Audiobookshelf does not expose an equivalent annotation domain;
- playlists and Audiobookshelf collections;
- covers from the live API or artwork from backups;
- PDF or comic reading locations that cannot be represented as EPUB CFIs;
- reader preferences, playback devices, server settings, or Audiobookshelf permissions; and
- continuous or two-way synchronization after the migration.
## Live API mode
Use live mode when BookOrbit can reach the running Audiobookshelf server.
1. Enter a clean `http` or `https` origin such as `https://audiobookshelf.example.com`. Do not include
credentials, a path, query parameters, or a fragment.
2. Enter an access token belonging to an Audiobookshelf `admin` or `root` user.
3. Enable private-network access only when the Audiobookshelf hostname resolves to a trusted local or
private address.
4. Test the connection, save the source, and validate it before creating the dry run.
BookOrbit resolves and pins the destination address for each request, rejects redirects, applies request
timeouts and response-size limits, and rejects private or local destinations unless private-network
access is explicitly enabled. Use HTTPS when traffic crosses an untrusted network. In Docker,
`127.0.0.1` refers to the BookOrbit container itself; use a hostname or address reachable from that
container.
The API token is redacted from migration API responses. It must never be placed in logs, screenshots,
reports, or issue comments.
## Backup mode
Use backup mode when BookOrbit should read an Audiobookshelf `.audiobookshelf` backup from local storage.
1. Create a fresh backup with Audiobookshelf's backup feature.
2. Create a dedicated host directory for migration inputs and copy the backup into it.
3. Mount that directory read-only into the BookOrbit container. For example, add this entry to the app
service's `volumes` list in `docker-compose.yml`:
```yaml
- /host/path/to/migration-imports:/imports:ro
```
4. Set `MIGRATION_IMPORT_ROOT=/imports` in BookOrbit's `.env` and restart BookOrbit.
5. In the migration source form, enter the absolute container path, such as
`/imports/audiobookshelf/backup.audiobookshelf` if that path exists under the mounted root.
`MIGRATION_IMPORT_ROOT` is a server-side authorization boundary. It must be an absolute path and cannot
be supplied through the migration request. BookOrbit resolves the configured root and backup path,
rejects paths and symlink targets outside the root, detects path swaps, requires a regular file, and
reads the archive through the already-authorized file handle. Use a dedicated read-only mount rather
than a broad filesystem root.
A compatible backup must be a ZIP archive containing exactly one `details` entry and one
`absdatabase.sqlite` entry. Archive entry counts and extracted sizes are bounded. BookOrbit copies only
the SQLite database to an owner-only temporary file and opens it read-only with SQLite defensive mode.
## Book and user mapping
Audiobookshelf stores paths as seen by the Audiobookshelf process. BookOrbit stores paths as seen by the
BookOrbit process. Add a prefix mapping when those paths differ. For example:
| Source prefix | Target prefix |
| ------------- | ------------------- |
| `/audiobooks` | `/books/audiobooks` |
Mappings replace only the matching leading prefix. More specific prefixes take priority. Save and
validate the mappings before starting the live run. Path mapping is one matching strategy; ISBN, ASIN,
and title/author matching can still match books whose paths differ.
Review every suggested user mapping. Users left unmapped remain visible in the mapping workflow and
migration report, but their state is not imported. Do not map multiple people to one BookOrbit user
merely to clear a warning.
## Recommended workflow
1. Back up BookOrbit and Audiobookshelf.
2. Scan the target BookOrbit libraries and confirm the target book files are present.
3. Create and test an Audiobookshelf source in live API or backup mode.
4. Save the source and validate its capabilities.
5. Map source users to existing BookOrbit users.
6. Add and validate path mappings where mount paths differ.
7. Run a dry run and review matches, ambiguities, unmatched books, unmapped users, warnings, and per-user
totals.
8. Resolve ambiguous matches and rerun the dry run until the plan is current.
9. Start the live migration and export the final report.
10. Verify several audiobook, EPUB, mixed-format, finished, and bookmarked examples before retiring the
Audiobookshelf instance.
Repeating the same completed plan is designed to be idempotent, but it is not a substitute for reviewing
the first report and keeping backups.
## Troubleshooting
### The live server is unreachable
- Confirm the URL contains only an `http` or `https` origin and does not redirect.
- Test DNS and network reachability from the BookOrbit server or container, not only from a browser.
- In Docker, do not use `127.0.0.1` for a service running in another container or on the host.
- Confirm a reverse proxy permits the Audiobookshelf API endpoints and does not redirect them to a login
page.
- A request times out after the bounded connector timeout. Investigate routing, DNS, firewall, or proxy
behavior before retrying.
### Audiobookshelf privileges are insufficient
The access token must authorize as an Audiobookshelf `admin` or `root` user. Create or select an account
with that type, obtain its token, and retest the source. BookOrbit intentionally rejects tokens that can
read only the current user's state because a complete migration must enumerate users and their state.
### A private or local address is rejected
Private-network access is off by default. Enable it only when the configured Audiobookshelf destination
is trusted and intentionally hosted on a private or local network. BookOrbit validates every resolved
address and pins the selected address for the request to limit DNS rebinding.
### A backup path is missing or rejected
- Confirm `MIGRATION_IMPORT_ROOT` is set in the BookOrbit process and is absolute.
- Use the path inside the BookOrbit container, not the host path or a browser workstation path.
- Confirm the backup exists under the canonical import root and is readable by the BookOrbit process.
- Prefer a direct regular-file path. A symlink target that resolves outside the root is rejected.
- If the container uses a read-only root filesystem, mount the import directory explicitly as shown
above; BookOrbit uses its writable temporary directory for bounded extraction.
### A backup is incompatible
Create a fresh backup with the verified Audiobookshelf version and retry. The backup must include valid
`details` and `absdatabase.sqlite` entries and the required users, library items, books, and media
progress schema. Missing optional author, series, listening-session, or library-folder tables are
reported as warnings. Missing required schema fails validation.
Do not manually modify the archive or SQLite schema to bypass validation. Preserve the original backup
and report the Audiobookshelf version and sanitized validation message when requesting compatibility
support.
### Books or users remain unresolved
- Confirm the target library scan completed before the dry run.
- Add a path-prefix mapping if the two applications see different mount paths.
- Check ISBN and ASIN metadata on both sides.
- Resolve ambiguous book matches explicitly in the dry-run workflow.
- Create the target BookOrbit user before mapping. The migration does not create accounts.
+290
View File
@@ -0,0 +1,290 @@
# Migrating from Calibre-Web Automated
BookOrbit can perform a one-time import from Calibre-Web Automated, abbreviated CWA. The migration
matches CWA books to books that already exist in BookOrbit, maps CWA users to existing BookOrbit users,
and imports supported reading state and static shelves.
This is an import, not continuous synchronization. Keep the original CWA installation and both systems'
backups until you have reviewed the migration report and verified the imported state.
## Compatibility
Compatibility is declared only for snapshots covered by the disposable end-to-end suite.
| Snapshot producer | Verified result |
| ----------------- | -------------------------------------------------------------------------------------- |
| CWA v4.0.6 | Full stopped-snapshot import, repeat-run idempotency, user isolation, and DB integrity |
CWA's `app.db` and Calibre's `metadata.db` do not contain a trustworthy CWA release number. BookOrbit
therefore reports the source version as unknown and emits this expected warning:
```text
Schema compatibility was verified against Calibre-Web Automated v4.0.6
```
That warning does not mean validation failed. Other CWA versions may work, but they are not currently
declared compatible. Missing optional tables produce precise warnings and disable only the affected
domain. Missing core book, file, or user columns cause validation to fail.
Maintainers can exercise another literal published image tag with:
```bash
CWA_E2E_IMAGE=crocodilestick/calibre-web-automated:<published-tag> \
pnpm e2e:run -- migration-calibre-web-automated
```
CWA's historical Docker tag casing is not consistent. Use the exact published tag rather than building
one from a version number. Do not broaden the compatibility declaration until the complete suite passes.
## Requirements
- The BookOrbit operator must have the `Manage App Settings` permission.
- Target books must already exist in a scanned BookOrbit library. Unmatched source books are reported
and are not created automatically.
- Target BookOrbit user accounts must already exist. CWA users are mapping candidates only; accounts,
passwords, roles, tokens, sessions, and permissions are never copied.
- BookOrbit must have a dedicated server-side import directory configured through
`MIGRATION_IMPORT_ROOT`.
- CWA must be fully stopped while `app.db` and its corresponding `metadata.db` are copied.
The two databases are independent SQLite files. Neither CWA nor BookOrbit can make a live copy of them
represent one shared instant. BookOrbit can detect active journal or WAL files beside the submitted
snapshots, but it cannot prove that snapshots copied elsewhere were taken consistently. Stopping CWA
before the copy is therefore required even when validation succeeds.
## Supported data
The first compatibility declaration covers:
- non-Guest CWA users as explicit mapping candidates, including administrators and regular users;
- Calibre books and individual formats;
- title, ordered authors and sort names, ISBN, ASIN and supported provider identifiers, publisher,
publication year, language, series, rating, description, and tags;
- unread, reading, and read status with available start, finish, and update dates;
- web-reader EPUB CFI resume position;
- web-reader audio position converted from milliseconds to seconds;
- web-reader comic position converted from CWA's zero-based image index to BookOrbit's one-based page;
- Kobo EPUB or KEPUB progress, preferring KEPUB when both formats exist;
- KOReader percentage and compatible EPUB locator when CWA's checksum index maps the document uniquely;
- static shelves and their relative book order; and
- matching by ISBN, ASIN, mapped file path, then title and author.
Calibre tags are imported as BookOrbit tags, not genres. Metadata is applied as an overlay: a field CWA
actually carries can replace that field on the matched BookOrbit book, while unavailable source fields
are left unchanged.
CWA's table named `bookmark` stores one current web-reader resume position. It does not represent a
collection of named bookmarks, so those rows become reading progress rather than BookOrbit bookmarks.
## Deferred and unavailable data
The following data is not imported:
- the default Guest account or state owned only by Guest;
- account credentials, OAuth data, API tokens, mail secrets, sessions, device secrets, or Hardcover
tokens;
- user creation, roles, permissions, or authentication settings;
- narrators and genres, because CWA does not distinguish them reliably from Calibre tags;
- reading-session history;
- named bookmarks and annotations;
- covers;
- Magic Shelves, dynamic shelf rules, and public-sharing behavior;
- arbitrary Calibre custom columns;
- CWA operational data from `cwa.db`; and
- continuous or two-way synchronization after the migration.
A public CWA shelf is imported as a private collection owned by the mapped BookOrbit user. It is never
granted to another user automatically.
## Create a stopped-instance snapshot
The common CWA container paths are:
| Data | Typical CWA container path |
| -------------------- | ------------------------------ |
| CWA application data | `/config/app.db` |
| Calibre catalog | `/calibre-library/metadata.db` |
Use the host paths bound to those container locations. If CWA selects a library below another folder,
copy the `metadata.db` belonging to the library currently configured in CWA, not another Calibre
catalog found on the host.
1. Back up the CWA config and Calibre library using your normal backup process.
2. Stop CWA completely. With the standard Compose service name:
```bash
docker compose stop calibre-web-automated
```
3. Confirm the container is stopped. Replace the name if your deployment uses another container name:
```bash
docker inspect -f '{{.State.Running}}' calibre-web-automated
```
The command must print `false`.
4. Inspect both source directories for non-empty SQLite sidecars. Check for `app.db-wal`,
`app.db-journal`, `metadata.db-wal`, and `metadata.db-journal`. Do not copy only a main database while
one of these contains data. Do not delete a sidecar to make validation pass. Restart CWA, allow its
recovery and startup work to finish, stop it cleanly, and check again.
5. While CWA remains stopped, copy exactly the two main database files into a dedicated BookOrbit import
directory. For example:
```bash
install -d -m 700 /host/path/to/bookorbit-migration-imports/cwa
install -m 600 /host/path/to/cwa-config/app.db \
/host/path/to/bookorbit-migration-imports/cwa/app.db
install -m 600 /host/path/to/calibre-library/metadata.db \
/host/path/to/bookorbit-migration-imports/cwa/metadata.db
```
6. Confirm the copied files are different regular files and that no copied `-wal` or `-journal`
sidecars exist beside them.
7. CWA may be restarted after both copies are complete. BookOrbit reads only the copies.
If CWA was never allowed to finish startup migrations, optional KOReader tables may not exist. When
KOReader progress matters, let the same CWA version start fully and finish its checksum work before
stopping and recopying the databases.
## Configure the BookOrbit import root
Mount only the dedicated import directory read-only into the BookOrbit app container. For example, add
this entry to the app service's `volumes` list in `docker-compose.yml`:
```yaml
- /host/path/to/bookorbit-migration-imports:/imports:ro
```
Set the corresponding absolute container path in BookOrbit's `.env`:
```dotenv
MIGRATION_IMPORT_ROOT=/imports
```
Restart BookOrbit so it receives the environment value and mount. In the migration source form, select
Calibre-Web Automated and enter:
```text
Application database: /imports/cwa/app.db
Calibre metadata database: /imports/cwa/metadata.db
```
`MIGRATION_IMPORT_ROOT` is a server-side authorization boundary. It cannot be supplied or overridden by
the migration request. BookOrbit requires absolute paths, resolves the configured root and both files,
rejects symlinks or path traversal, requires two different regular files, copies them into owner-only
temporary storage, opens them read-only with SQLite defensive mode, runs integrity checks, and cleans up
the private copies after each operation.
Use paths as seen by the BookOrbit process or container. A host path that is not mounted at the same
location is not a valid migration path. Do not use `/`, a home directory, the CWA config directory, or a
broad storage mount as the import root.
## Book path mapping
The snapshot file location is not the source library root used for book matching. Book paths are based
on the logical Calibre library root stored in CWA's `app.db`, normally `/calibre-library`. A split-library
configuration may provide a different logical root. BookOrbit reports the detected prefix after source
validation.
Map that CWA-visible prefix to the path where the same files exist inside BookOrbit. For example:
| Source prefix | Target prefix |
| ------------------ | ------------- |
| `/calibre-library` | `/books` |
Mappings replace only the matching leading path prefix. The target prefix is not the host path unless
BookOrbit sees that exact path. Validate the mapping before creating the final dry run.
CWA does not provide BookOrbit file hashes, so mapped paths are especially important for books with
multiple files of the same media kind, such as EPUB and KEPUB together. ISBN, ASIN, and title/author
matching can still match books whose paths differ, but exact mapped paths give file-level progress the
strongest target-file signal.
## User mapping and recommended workflow
Review every suggested user mapping. Users left unmapped remain visible in source counts and reports,
but their status, progress, and shelves are not imported. Never map multiple people to one BookOrbit
account merely to clear a warning.
1. Back up BookOrbit and CWA.
2. Scan the target BookOrbit libraries and confirm the target files are present.
3. Stop CWA and create both snapshots as described above.
4. Configure the read-only import mount and `MIGRATION_IMPORT_ROOT`, then restart BookOrbit.
5. Create a Calibre-Web Automated source with the two snapshot paths.
6. Test the source, save it, and validate its capabilities and warnings.
7. Map selected CWA users to existing BookOrbit users.
8. Add and validate the logical-library path mapping.
9. Run a dry run and review match strategies, ambiguities, unmatched books, unmapped users, warnings,
unavailable domains, and per-user totals.
10. Resolve ambiguous matches and create a current dry run.
11. Start the live migration and export the final report.
12. Verify representative EPUB, Kobo or KEPUB, KOReader, audio, comic, finished-status, and ordered-shelf
examples before retiring the CWA installation or snapshots.
Repeating the same completed plan is designed to be idempotent, but it is not a substitute for reviewing
the first report and retaining backups.
## Troubleshooting
### A snapshot is reported as active or hot
BookOrbit rejects a submitted database when a non-empty `-wal` or `-journal` file exists beside it. Stop
CWA, discard the attempted copies, and create a fresh pair from the stopped installation. Do not remove
sidecars from a running installation or copy only the main database.
If the original stopped directories still contain non-empty sidecars, restart the same CWA installation,
allow recovery to complete, stop it cleanly, and inspect again. Preserve the original files and request
support if they remain; do not repair production databases in place merely to satisfy migration.
### The two database paths appear reversed
The application field must point to CWA's `app.db`, which contains the `user` table. The metadata field
must point to Calibre's `metadata.db`, which contains `books` and `data`. Correct the two fields and
retest. Renaming the wrong files does not make them compatible.
### A path is missing, outside the import root, or unreadable
- Confirm `MIGRATION_IMPORT_ROOT` is set in the BookOrbit process and is absolute.
- Use paths inside the BookOrbit container, not host-only or workstation paths.
- Confirm the read-only mount contains both files and the BookOrbit container user can read them.
- Keep both snapshots below the canonical import root and avoid symlinks.
- Confirm the paths identify two different regular files.
### The snapshot schema is incompatible
Confirm the files came from the same stopped CWA installation and that CWA had completed startup. Missing
required user, book, or file tables or columns fail validation. Do not edit SQLite tables or bypass
validation. Preserve the original snapshot pair and report the exact CWA image tag plus the sanitized
validation message when requesting compatibility support.
### Optional progress or shelves are unavailable
Missing `book_read_link`, web bookmark, Kobo, KOReader, checksum, or shelf tables disable only the related
domain and produce a warning. This can be legitimate when the feature was never used. If the source
installation should contain that data, start the same CWA version, allow its migrations and checksum work
to finish, stop it, and create a new snapshot pair.
### No CWA-specific schema signature is found
BookOrbit warns when the snapshot has compatible core Calibre-Web tables but none of the known CWA
signatures. The files may come from stock Calibre-Web or an unverified CWA schema. Review the warning and
source provenance carefully. A core-schema match is not proof of CWA compatibility.
### The logical library path is unsafe or missing
BookOrbit rejects traversal, backslashes, NULs, and unsafe absolute or relative path components. The
logical root comes from CWA configuration, not from the snapshot's host directory. Correct the library
configuration in CWA, let it start successfully, stop it, and create fresh snapshots rather than editing
the copies.
### Books or users remain unresolved
- Confirm the target BookOrbit library scan completed before the dry run.
- Map the CWA logical library prefix to the path visible inside BookOrbit.
- Check ISBN and ASIN metadata on both sides.
- Check title and author spelling when relying on the final fallback strategy.
- Resolve ambiguous book matches explicitly in the dry-run workflow.
- Create the target BookOrbit account before mapping. The migration does not create users.
Unmatched books remain in the report and do not prevent valid matched books from importing.
+22 -20
View File
@@ -120,26 +120,28 @@ check that your env vars match the values in `server/.env.example`.
### Suite reference
| Suite ID | Lane | DB mode | Timeout | Description |
| ---------------------------- | ----- | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------- |
| `guard-mechanics` | smoke | default-db | 30 min | `@Public` / `@RequirePermission` guard wiring; uses its own minimal NestJS testing module, not the shared app harness |
| `scanner-scenarios` | full | dedicated-db | 45 min | File organization scenario matrix for the library scanner |
| `scanner-file-operations` | full | dedicated-db | 60 min | File add/remove/rename operations and scanner reconciliation |
| `auth-session-security` | full | dedicated-db | 50 min | Cookie flags, token rotation, concurrent sessions |
| `auth-recovery-oidc-logout` | full | dedicated-db | 50 min | Password reset flow and OIDC logout |
| `book-dock-ingest-finalize` | full | dedicated-db | 45 min | Book Dock upload ingest and finalize flow |
| `metadata-write` | full | dedicated-db | 60 min | Metadata write operations (field updates, bulk edits) |
| `metadata-lock` | full | dedicated-db | 60 min | Metadata lock enforcement across operations |
| `migration-booklore` | full | dedicated-db | 70 min | Booklore v1 to BookOrbit data migration |
| `authorization-matrix` | full | dedicated-db | 60 min | Permission enforcement across all protected routes |
| `book-api-contract` | full | dedicated-db | 60 min | Book API response shape and field contract |
| `app-settings-oidc-contract` | full | dedicated-db | 60 min | App settings and OIDC configuration contract |
| `library-admin-workflows` | full | dedicated-db | 60 min | Library create/update/delete admin workflows |
| `reader-format-delivery` | full | dedicated-db | 60 min | Book file serving and format delivery |
| `opds-auth-catalog` | full | dedicated-db | 45 min | OPDS feed authentication and catalog structure |
| `email-lifecycle` | full | dedicated-db | 45 min | Email provider, template, and send lifecycle |
| `reader-state-isolation` | full | dedicated-db | 45 min | Reading progress, bookmarks, and annotations isolated per user |
| `users-admin-lifecycle` | full | dedicated-db | 50 min | User create/update/delete admin lifecycle |
| Suite ID | Lane | DB mode | Timeout | Description |
| --------------------------------- | ----- | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------- |
| `guard-mechanics` | smoke | default-db | 30 min | `@Public` / `@RequirePermission` guard wiring; uses its own minimal NestJS testing module, not the shared app harness |
| `scanner-scenarios` | full | dedicated-db | 45 min | File organization scenario matrix for the library scanner |
| `scanner-file-operations` | full | dedicated-db | 60 min | File add/remove/rename operations and scanner reconciliation |
| `auth-session-security` | full | dedicated-db | 50 min | Cookie flags, token rotation, concurrent sessions |
| `auth-recovery-oidc-logout` | full | dedicated-db | 50 min | Password reset flow and OIDC logout |
| `book-dock-ingest-finalize` | full | dedicated-db | 45 min | Book Dock upload ingest and finalize flow |
| `metadata-write` | full | dedicated-db | 60 min | Metadata write operations (field updates, bulk edits) |
| `metadata-lock` | full | dedicated-db | 60 min | Metadata lock enforcement across operations |
| `migration-booklore` | full | dedicated-db | 70 min | Booklore v1 to BookOrbit data migration |
| `migration-audiobookshelf` | full | dedicated-db | 90 min | Audiobookshelf live API and real-backup migration |
| `migration-calibre-web-automated` | full | dedicated-db | 120 min | Calibre-Web Automated stopped-snapshot migration |
| `authorization-matrix` | full | dedicated-db | 60 min | Permission enforcement across all protected routes |
| `book-api-contract` | full | dedicated-db | 60 min | Book API response shape and field contract |
| `app-settings-oidc-contract` | full | dedicated-db | 60 min | App settings and OIDC configuration contract |
| `library-admin-workflows` | full | dedicated-db | 60 min | Library create/update/delete admin workflows |
| `reader-format-delivery` | full | dedicated-db | 60 min | Book file serving and format delivery |
| `opds-auth-catalog` | full | dedicated-db | 45 min | OPDS feed authentication and catalog structure |
| `email-lifecycle` | full | dedicated-db | 45 min | Email provider, template, and send lifecycle |
| `reader-state-isolation` | full | dedicated-db | 45 min | Reading progress, bookmarks, and annotations isolated per user |
| `users-admin-lifecycle` | full | dedicated-db | 50 min | User create/update/delete admin lifecycle |
JUnit XML output for each suite is written to `test-results/server/<suite-id>-e2e-junit.xml`.