Files
Libation/Source/_Tests/FileLiberator.Tests/DownloadPdfVerificationTests.cs
T
Cursor Agentandrmcrackan 537f53d856 fix(pdf): do not save a non-PDF response as a title's PDF
That the file existed said only that the server sent a body, and an Audible error is
a 200 with a JSON body like any other response. Dinah's downloader renames by
Content-Disposition, so such a body landed in the book's folder under whatever
Audible called it and the title was recorded as having its PDF.

Check the payload: a file named .pdf must carry the PDF header, and nothing may begin
with the opening character of a JSON or markup document. A rejected download is
deleted rather than left in the library, which also lets the empty-folder cleanup
run, and the file is added to the path cache only once it has passed.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 23:53:44 +00:00

99 lines
3.3 KiB
C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Text;
namespace FileLiberator.Tests;
/// <summary>
/// What counts as a supplement having been downloaded. Reported in issue #1947: an Audible response that was
/// not the PDF was saved into the book's folder and the title recorded as having its PDF.
/// </summary>
[TestClass]
public class DownloadPdfVerificationTests
{
private string directory = string.Empty;
[TestInitialize]
public void Initialize()
{
directory = Path.Combine(Path.GetTempPath(), $"libation-pdf-verify-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
}
[TestCleanup]
public void Cleanup()
{
try
{
Directory.Delete(directory, recursive: true);
}
catch (IOException)
{
// A leftover temp directory is not worth failing a test over.
}
}
private string Write(string fileName, byte[] contents)
{
var path = Path.Combine(directory, fileName);
File.WriteAllBytes(path, contents);
return path;
}
private string Write(string fileName, string contents) => Write(fileName, Encoding.UTF8.GetBytes(contents));
private static byte[] Pdf => Encoding.ASCII.GetBytes("%PDF-1.4\n1 0 obj\n<< >>\nendobj\n");
[TestMethod]
public void A_real_pdf_passes()
=> Assert.IsTrue(DownloadPdf.verifyDownload(Write("book.pdf", Pdf)).IsSuccess);
[TestMethod]
public void A_file_that_is_not_there_fails()
=> Assert.IsFalse(DownloadPdf.verifyDownload(Path.Combine(directory, "never-written.pdf")).IsSuccess);
[TestMethod]
public void An_empty_file_fails()
=> Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.pdf", Array.Empty<byte>())).IsSuccess);
[TestMethod]
public void A_file_named_pdf_that_is_not_a_pdf_fails()
=> Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.pdf", "Not a PDF at all")).IsSuccess);
/// <summary>
/// The reported failure. Dinah's downloader renames by Content-Disposition, so an Audible JSON body does
/// not even arrive named .pdf and an extension check alone would wave it through.
/// </summary>
[TestMethod]
public void An_audible_json_body_fails_whatever_it_is_named()
{
const string body = """{"asin":"B089T8FSK6","asset_details":[],"is_preview_enabled":false,"is_vvab":false}""";
Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.json", body)).IsSuccess);
Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.pdf", body)).IsSuccess);
}
[TestMethod]
public void A_json_body_behind_a_byte_order_mark_fails()
=> Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.json", Encoding.UTF8.GetPreamble().Concat("""{"asin":"B089T8FSK6"}"""))).IsSuccess);
[TestMethod]
public void A_login_page_fails()
=> Assert.IsFalse(DownloadPdf.verifyDownload(Write("book.html", "<!DOCTYPE html><html><body>Sign in</body></html>")).IsSuccess);
/// <summary>
/// A supplement is whatever its URL says it is, so a non-PDF one is only rejected when it looks like a
/// document Audible sent instead of the file. This is a zip's signature.
/// </summary>
[TestMethod]
public void A_non_pdf_supplement_passes()
=> Assert.IsTrue(DownloadPdf.verifyDownload(Write("book.zip", [0x50, 0x4B, 0x03, 0x04, 0x14, 0x00])).IsSuccess);
}
file static class ByteExtensions
{
public static byte[] Concat(this byte[] first, string second)
=> [.. first, .. Encoding.UTF8.GetBytes(second)];
}