using System; using System.Collections.Generic; using System.IO; using System.Linq; using AssertionHelper; using DataLayer; using LibationSearchEngine; using Microsoft.VisualStudio.TestTools.UnitTesting; using Directory = System.IO.Directory; namespace SearchEngineTests; /// /// Subtitle removal is all-or-nothing: <title short> stops at the first colon, so "Omnibus: Volume One" /// and "Omnibus: Volume Two" both become "Omnibus". Finding the affected books used to be impossible -- the analyzer /// throws punctuation away and Lucene reads a colon in a query as a field separator -- so the index flags the two /// separate ways a name can lose part of its title. /// [TestClass] public class SubtitleSearchFieldTests { private const string PLAIN = "B0PLAIN0001"; private const string SUBTITLE = "B0SUBTITL01"; private const string COLON = "B0COLON0001"; private const string BOTH = "B0BOTH00001"; private string indexDirectory = null!; [TestInitialize] public void Initialize() { indexDirectory = Path.Combine(Path.GetTempPath(), "LibationSearchEngineTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(indexDirectory); new SearchEngine(indexDirectory).CreateNewIndex(library); } [TestCleanup] public void Cleanup() { try { if (Directory.Exists(indexDirectory)) Directory.Delete(indexDirectory, recursive: true); } catch (IOException) { // Windows refuses to delete a file Lucene still holds open, and a leftover temp directory is not // worth failing a test over } } private static LibraryBook book(string asin, string title, string? subtitle) { var contributor = Contributor.GetEmpty(); var b = new Book(new AudibleProductId(asin), title, subtitle, null, 1, ContentType.Product, [contributor], [contributor], "us"); return new LibraryBook(b, new DateTime(2026, 8, 15), "account"); } private static readonly List library = [ book(PLAIN, "Sign of the Four", null), book(SUBTITLE, "A Book Series Omnibus", "Volume One"), book(COLON, "Star Trek: The Next Generation", null), book(BOTH, "Dune: Book One", "The Graphic Novel") ]; private string[] search(string query) => [.. new SearchEngine(indexDirectory).Search(query).Docs.Select(d => d.ProductId)]; /// Audible's own subtitle field, which every template except <title> leaves out. [TestMethod] [DataRow("HasSubtitle")] [DataRow("HasSubtitles")] [DataRow("hassubtitle")] public void books_with_an_audible_subtitle_are_found(string query) => search(query).Should().BeEquivalentTo([SUBTITLE, BOTH]); /// The riskier case: the colon is inside Audible's title, so shortening cuts the title itself. [TestMethod] [DataRow("TitleHasColon")] [DataRow("ColonInTitle")] [DataRow("titlehascolon")] public void books_whose_title_contains_a_colon_are_found(string query) => search(query).Should().BeEquivalentTo([COLON, BOTH]); [TestMethod] public void the_two_fields_are_independent() { search("HasSubtitle AND TitleHasColon").Should().BeEquivalentTo([BOTH]); search("HasSubtitle OR TitleHasColon").Should().BeEquivalentTo([SUBTITLE, COLON, BOTH]); } /// The complement is what makes this useful: everything shortening cannot damage. [TestMethod] public void books_a_short_title_leaves_alone_are_found_by_negation() { search("-HasSubtitle").Should().BeEquivalentTo([PLAIN, COLON]); search("-TitleHasColon").Should().BeEquivalentTo([PLAIN, SUBTITLE]); search("-HasSubtitle AND -TitleHasColon").Should().BeEquivalentTo([PLAIN]); } /// Bool fields combine with the rest of the syntax, which is the point: filter, then liberate. [TestMethod] public void the_fields_combine_with_other_search_terms() => search("TitleHasColon AND title:dune").Should().BeEquivalentTo([BOTH]); /// A subtitle is still part of the title text, so searching for its words keeps working. [TestMethod] public void subtitle_text_remains_searchable() => search("title:\"volume one\"").Should().BeEquivalentTo([SUBTITLE]); }