Files
Libation/Source/_Tests/FileLiberator.Tests/DownloadPdfLicenseTests.cs
T
Cursor Agentandrmcrackan d4b5a48b79 test: cover the supplement download's license, status and selection rules
DownloadPdfLicenseTests drives the step with and without a license in hand. No
Audible account exists in those tests, so a test that completes at all is the
proof that the step did not go asking for a license of its own.

BulkRunSelectionTests drives the real run loop against a real library database:
which titles each pass reaches, that an absent title is left alone by both, that
--force still attempts it, and that a refused title is waited on by the follow-up
pass and reported once rather than twice.

SupplementSyncTests covers the scan rules, in a new DtoImporterService.Tests
project. The two replaced assertions - that a PDF-only title is never waited on,
and that a --pdf run is never held back - encoded the belief that a PDF is a
different request from the audiobook, which is what this branch disproves.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-19 18:48:07 +00:00

175 lines
5.7 KiB
C#

using ApplicationServices;
using AudibleApi.Common;
using DataLayer;
using LibationFileManager;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileLiberator.Tests;
/// <summary>
/// How the supplement download treats the content license it is given. Reported in issue #1973: a scheduled run
/// asked Audible for a license for the same titles every 15 minutes, because the step asked for its own license
/// every time and remembered nothing about what came back.
/// <para>
/// No Audible account exists in these tests, so <c>GetApiAsync</c> throws. That is the point: a test that
/// completes proves the step never went to Audible for a license of its own.
/// </para>
/// </summary>
[TestClass]
[DoNotParallelize]
public class DownloadPdfLicenseTests
{
private string tempLibationFiles = string.Empty;
[TestInitialize]
public void Initialize()
{
tempLibationFiles = Path.Combine(Path.GetTempPath(), $"libation-pdf-license-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempLibationFiles);
Environment.SetEnvironmentVariable(LibationFiles.LIBATION_FILES_DIR, tempLibationFiles);
Configuration.CreateMockInstance();
}
[TestCleanup]
public void Cleanup()
{
Configuration.RestoreSingletonInstance();
Environment.SetEnvironmentVariable(LibationFiles.LIBATION_FILES_DIR, null);
try
{
Directory.Delete(tempLibationFiles, recursive: true);
}
catch (IOException)
{
// A leftover temp directory is not worth failing a test over.
}
}
private static int seeded;
/// <summary>
/// A title Audible lists a supplement for, as a real row: writing off a PDF is a change to the database, so
/// there has to be something there to change.
/// </summary>
private static LibraryBook BookWithPdf(LiberatedStatus pdfStatus = LiberatedStatus.NotLiberated)
{
var book = new Book(
new AudibleProductId($"B0PDFLIC{++seeded:0000}"),
"Supplemented Title",
"",
"",
600,
DataLayer.ContentType.Product,
[new Contributor("Test Author")],
[new Contributor("Test Narrator")],
"us");
book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
book.SetSupplementDownloadUrl("https://example.com/supplement.pdf");
book.UserDefinedItem.SetPdfStatus(pdfStatus);
var libraryBook = new LibraryBook(book, new DateTime(2026, 1, 1), "test-account");
using var context = DbContexts.GetContext();
context.LibraryBooks.Add(libraryBook);
context.SaveChanges();
return libraryBook;
}
/// <summary>
/// A license Audible granted, carrying a supplement link or not. No content metadata: a supplement needs
/// nothing from a license but its pdf_url, which is why requiring metadata belongs to the audiobook step.
/// </summary>
private static DownloadOptions.LicenseInfo License(string? pdfUrl)
=> DownloadOptions.LicenseInfo.Create(new ContentLicense
{
Asin = "B002V5B8OY",
StatusCode = "Granted",
PdfUrl = pdfUrl
});
private static DownloadPdf NewStep(DownloadOptions.LicenseInfo? carried = null)
{
var step = DownloadPdf.Create(Configuration.Instance);
step.LicenseInfo = carried;
return step;
}
[TestMethod]
public async Task A_license_that_carries_no_supplement_writes_the_pdf_off()
{
// Audible granting a license and returning no pdf_url is Audible saying the title has no PDF. Error is
// what the audiobook download has always used to mean "don't attempt this again", and until now the
// supplement had no way of saying it - so every run asked, and every run got the same answer.
var libraryBook = BookWithPdf();
var status = await NewStep(License(pdfUrl: null)).ProcessAsync(libraryBook);
Assert.IsTrue(status.IsSuccess, string.Join(", ", status.Errors));
Assert.AreEqual(LiberatedStatus.Error, libraryBook.Book.UserDefinedItem.PdfStatus);
}
[TestMethod]
public async Task A_written_off_pdf_is_not_selected_again()
{
var libraryBook = BookWithPdf();
await NewStep(License(pdfUrl: null)).ProcessAsync(libraryBook);
Assert.IsFalse(NewStep().Validate(libraryBook));
}
[TestMethod]
public void A_pdf_marked_as_an_error_is_skipped_the_way_an_errored_audiobook_is()
{
// DownloadDecryptBook.Validate goes through AudioExists, which counts Error, so the audiobook step has
// always left an errored title alone. The supplement step selected on PdfExists, which does not.
Assert.IsFalse(NewStep().Validate(BookWithPdf(LiberatedStatus.Error)));
Assert.IsTrue(NewStep().Validate(BookWithPdf(LiberatedStatus.NotLiberated)));
Assert.IsFalse(NewStep().Validate(BookWithPdf(LiberatedStatus.Liberated)));
}
[TestMethod]
public async Task The_license_in_hand_is_published_for_the_next_step()
{
var carried = License(pdfUrl: null);
var step = NewStep(carried);
await step.ProcessAsync(BookWithPdf());
Assert.AreSame(carried, step.ObtainedLicense);
}
[TestMethod]
public async Task A_step_with_no_license_asks_Audible_for_one()
{
// The other half of the arrangement: a PDF-only run has no license to be handed, so it requests one.
// With no account configured that request cannot be made, which is how this test can tell it was tried.
var status = await NewStep().ProcessAsync(BookWithPdf());
Assert.IsFalse(status.IsSuccess);
StringAssert.Contains(string.Join(", ", status.Errors), "Error downloading PDF");
}
[TestMethod]
public async Task A_license_left_over_from_another_title_is_not_reused()
{
// Processable instances are reused across books, so a step must not carry a license into a second
// attempt of its own accord.
var step = NewStep(License(pdfUrl: null));
await step.ProcessAsync(BookWithPdf());
step.LicenseInfo = null;
var second = await step.ProcessAsync(BookWithPdf());
Assert.IsFalse(second.IsSuccess);
Assert.IsNull(step.ObtainedLicense);
}
}