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;
///
/// 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.
///
/// No Audible account exists in these tests, so GetApiAsync throws. That is the point: a test that
/// completes proves the step never went to Audible for a license of its own.
///
///
[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;
///
/// 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.
///
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;
}
///
/// 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.
///
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);
}
}