docs/development/testing.md
How to run Libation's automated tests, and how to put the app into a known state so you can check a UI change by eye.
Test projects live under Source/_Tests/. They use MSTest on the Microsoft.Testing.Platform runner, which is selected by global.json.
That runner changes one thing about the usual command: dotnet test needs --project and rejects a bare project path.
# Run one project
dotnet test --project Source/_Tests/LibationUiBase.Tests/LibationUiBase.Tests.csproj
# Run everything, the way CI does
cd Source
dotnet test
Running from Source/ works on every platform, including Linux. It restores the whole solution but only builds the test projects and what they reference, so the Windows-only projects (LibationWinForms, HangoverWinForms) are never compiled and do not break the run. Naming a single project is just quicker and gives less output while you iterate.
Each one tests the source project of the same name:
| Project | Covers |
|---|---|
ApplicationServices.Tests | Library commands and queries, Audiobookshelf upload |
AudibleUtilities.Tests | Audible API wrappers, accounts, and token storage |
FileLiberator.Tests | Download, decrypt, and conversion steps |
FileManager.Tests | Paths, filename templates, and file utilities |
LibationFileManager.Tests | Configuration, settings, upgrades, and naming templates |
LibationSearchEngine.Tests | Lucene indexing and search syntax |
LibationUiBase.Tests | Shared grid view models and status icons |
AssertionHelper is a shared helper library, not a test project.
Scripts/ holds developer utilities. They exist only in a source checkout - none of them ship in an install - so this page is where they are documented.
The testing scripts are file-based C# apps: a single .cs file with its NuGet references declared at the top. There is no project to restore and nothing to add to the solution. Run one directly with the .NET SDK the repo already pins:
dotnet run Scripts/<script-name>.cs
The script path is the only thing tied to your working directory, so an absolute path works from anywhere. The first run takes a few seconds while the SDK fetches the script's packages.
To pass arguments to the script rather than to dotnet run, put them after --:
dotnet run Scripts/<script-name>.cs -- --some-flag
Forgetting the -- is the usual reason a flag appears to be ignored.
Fills a Libation library with fake books covering every icon the grid's Liberate column can draw, so a change to those icons can be checked at a glance instead of by hunting for a real book in the right state.
It seeds the full stoplight matrix - each lamp color, each PDF state, purchased and Audible Plus - plus both error icons and a podcast series with episodes. On success it prints a row-by-row list of what each seeded row should look like, so sort the grid by Title and read down.
Run Libation once first so the database exists, and close it before seeding:
dotnet run Scripts/seed-demo-library.cs
Remove the seeded books and their placeholder files:
dotnet run Scripts/seed-demo-library.cs -- --clean
The script looks for your Libation folder in the usual per-platform locations, including any redirect in an appsettings.json it finds. If it cannot locate the database, pass the folder:
dotnet run Scripts/seed-demo-library.cs -- "C:\Users\you\AppData\Local\Libation"
Both commands are safe to re-run. Seeding skips books that are already present, and only ever touches its own rows, which are prefixed DEMO.
::: warning The seeded books are not real, so do not click their stoplights - that queues a download which cannot succeed. Expanding a seeded series row is fine. :::
Two of the Liberate icons are not stored in the database at all, which is worth knowing before you try to add a state to the script or reproduce one by hand:
Liberated_Status reports PartialDownload when an .aaxc matching the book's ASIN is sitting in the InProgress folder. The status cannot be written directly either: UserDefinedItem's BookStatus setter rewrites PartialDownload to NotLiberated on the way in. The script writes placeholder .aaxc files for its yellow rows.AudioExists is defined as BookStatus is Liberated or Error, so neither needs an audio file to exist. Creating one changes nothing.One more trap, in the grid rather than the database: a podcast's series is identified by the parent book's own ASIN, not by an arbitrary series id, and SeriesEntry.GetAllSeriesEntriesAsync discards any series whose children it cannot match. Point the episodes at a different series id and the parent row disappears from the grid with no error.
Fills the DownloadHistory table with fake completed downloads, so the daily download limit can be tested without downloading anything. Start Libation once first so the table exists.
Reach a limit of 50 Audible Plus titles and stay there:
dotnet run Scripts/seed-download-history.cs -- --count 50 --age-seconds 7200
The limit uses a rolling 24 hour window, and --age-seconds is how long ago each fake download finished, so it also controls when the window frees up. Dating the rows just under 24 hours old turns a multi-day wait into a one minute wait, which is how the pause-and-resume behavior is checked:
# a paused queue resumes on its own about a minute from now
dotnet run Scripts/seed-download-history.cs -- --count 50 --age-seconds 86325
Other flags: --owned seeds purchased titles instead of Plus ones (useful for checking that a Plus-only limit ignores them), --mb sets the size of each fake download for testing MB and GB limits, and --clean deletes every row.
Libation may be running while you seed. Each check re-queries the database, so a running queue picks up the new rows within seconds without a restart.
::: tip The limit is off by default. Set Settings > Download/Decrypt > Daily download limit before expecting seeded rows to block anything. :::