--- /dev/null
+package net.pterodactylus.rhynodge.states
+
+import java.io.StringReader
+import javax.xml.parsers.DocumentBuilderFactory
+import net.pterodactylus.rhynodge.states.EpisodeState.Episode
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile
+import net.pterodactylus.util.collection.batch
+import net.pterodactylus.util.dom.XPathEvaluator
+import net.pterodactylus.util.test.isLinkNode
+import net.pterodactylus.util.test.isTextNode
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.anything
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.equalTo
+import org.junit.jupiter.api.Test
+import org.xml.sax.InputSource
+
+class EpisodeStateTest {
+
+ @Test
+ fun `episode state renders table with correct caption`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asString("/html/body/table/caption/text()"), equalTo("All Known Episodes"))
+ }
+ }
+
+ @Test
+ fun `episode state renders table with correct headers`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("/html/body/table/thead/tr/th/text()"), contains(
+ isTextNode("Season"),
+ isTextNode("Episode"),
+ isTextNode("Filename"),
+ isTextNode("Size"),
+ isTextNode("File(s)"),
+ isTextNode("Seeds"),
+ isTextNode("Leechers"),
+ isTextNode("Magnet"),
+ isTextNode("Download")
+ ))
+ }
+ }
+
+ @Test
+ fun `episode state renders one row per torrent file`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNumber("count(/html/body/table/tbody/tr)"), equalTo(6.0))
+ }
+ }
+
+ @Test
+ fun `episode state renders all episodes in correct order`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("/html/body/table/tbody/tr/td[position()=1 or position()=2]"), contains(
+ isTextNode("2"), isTextNode("1"),
+ isTextNode("1"), isTextNode("2"),
+ isTextNode(""), anything(),
+ isTextNode("1"), isTextNode("1"),
+ isTextNode(""), anything(),
+ isTextNode(""), anything(),
+ ))
+ }
+ }
+
+ @Test
+ fun `files are sorted in correct order`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("/html/body/table/tbody/tr/td[@class='filename']"), contains(
+ isTextNode("S02E01.720p.mkv"),
+ isTextNode("S01E02.720p.mkv"),
+ isTextNode("S01E02.1080p.mkv"),
+ isTextNode("S01E01.720p.mkv"),
+ isTextNode("S01E01.1080p.mkv"),
+ isTextNode("1x01.1080p.mkv"),
+ ))
+ }
+ }
+
+ @Test
+ fun `files are rendered correctly`() {
+ renderHtmlAndVerify(episodeState) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asNodeList("/html/body/table/tbody/tr/td[@class='filename']/text() | /html/body/table/tbody/tr/td[@class='filename']/following-sibling::td/child::node()[1]").batch(7), containsInAnyOrder(
+ contains(isTextNode("S01E01.720p.mkv"), isTextNode("111111111"), isTextNode("11"), isTextNode("111"), isTextNode("1111"), isLinkNode("Link", "magnet:S01E01.720p.mkv"), isLinkNode("Link", "url://S01E01.720p.mkv")),
+ contains(isTextNode("S01E01.1080p.mkv"), isTextNode("222222222"), isTextNode("22"), isTextNode("222"), isTextNode("2222"), isLinkNode("Link", "magnet:S01E01.1080p.mkv"), isLinkNode("Link", "url://S01E01.1080p.mkv")),
+ contains(isTextNode("1x01.1080p.mkv"), isTextNode("333333333"), isTextNode("33"), isTextNode("333"), isTextNode("3333"), isLinkNode("Link", "magnet:1x01.1080p.mkv"), isLinkNode("Link", "url://1x01.1080p.mkv")),
+ contains(isTextNode("S01E02.720p.mkv"), isTextNode("444444444"), isTextNode("44"), isTextNode("444"), isTextNode("4444"), isLinkNode("Link", "magnet:S01E02.720p.mkv"), isLinkNode("Link", "url://S01E02.720p.mkv")),
+ contains(isTextNode("S01E02.1080p.mkv"), isTextNode("555555555"), isTextNode("55"), isTextNode("555"), isTextNode("5555"), isLinkNode("Link", "magnet:S01E02.1080p.mkv"), isLinkNode("Link", "url://S01E02.1080p.mkv")),
+ contains(isTextNode("S02E01.720p.mkv"), isTextNode("666666666"), isTextNode("66"), isTextNode("666"), isTextNode("6666"), isLinkNode("Link", "magnet:S02E01.720p.mkv"), isLinkNode("Link", "url://S02E01.720p.mkv")),
+ ))
+ }
+ }
+
+ @Test
+ fun `filename is escaped correctly`() {
+ renderHtmlAndVerify(EpisodeState(listOf(Episode(1, 1).apply { addTorrentFile(TorrentFile("A&B", "1", "m", "d", 2, 3, 4)) }))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asString("/html/body/table/tbody/tr/td[@class='filename']/text()"), equalTo("A&B"))
+ }
+ }
+
+ @Test
+ fun `sizes is escaped correctly`() {
+ renderHtmlAndVerify(EpisodeState(listOf(Episode(1, 1).apply { addTorrentFile(TorrentFile("A", "&", "m", "d", 2, 3, 4)) }))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asString("/html/body/table/tbody/tr/td[@class='filename']/following-sibling::td[1]/text()"), equalTo("&"))
+ }
+ }
+
+ @Test
+ fun `magnet link is escaped correctly`() {
+ renderHtmlAndVerify(EpisodeState(listOf(Episode(1, 1).apply { addTorrentFile(TorrentFile("A", "1", "&", "d", 2, 3, 4)) }))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asString("/html/body/table/tbody/tr/td[@class='filename']/following-sibling::td[5]/a/@href"), equalTo("&"))
+ }
+ }
+
+ @Test
+ fun `download link is escaped correctly`() {
+ renderHtmlAndVerify(EpisodeState(listOf(Episode(1, 1).apply { addTorrentFile(TorrentFile("A", "1", "m", "&", 2, 3, 4)) }))) { xPathEvaluator ->
+ assertThat(xPathEvaluator.asString("/html/body/table/tbody/tr/td[@class='filename']/following-sibling::td[6]/a/@href"), equalTo("&"))
+ }
+ }
+
+ private fun renderHtmlAndVerify(episodeState: EpisodeState, xPathEvaluator: (xPathEvaluator: XPathEvaluator) -> Unit) {
+ val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(episodeState.htmlText())))
+ xPathEvaluator(XPathEvaluator(document))
+ }
+
+ private val episodeState = EpisodeState(episodes, listOf(episodes[2]), listOf(episodes[1]), listOf(torrentFiles[2], torrentFiles[4], torrentFiles[5]))
+
+}
+
+private val torrentFiles = listOf(
+ TorrentFile("S01E01.720p.mkv", "111111111", "magnet:S01E01.720p.mkv", "url://S01E01.720p.mkv", 11, 111, 1111),
+ TorrentFile("S01E01.1080p.mkv", "222222222", "magnet:S01E01.1080p.mkv", "url://S01E01.1080p.mkv", 22, 222, 2222),
+ TorrentFile("1x01.1080p.mkv", "333333333", "magnet:1x01.1080p.mkv", "url://1x01.1080p.mkv", 33, 333, 3333),
+ TorrentFile("S01E02.720p.mkv", "444444444", "magnet:S01E02.720p.mkv", "url://S01E02.720p.mkv", 44, 444, 4444),
+ TorrentFile("S01E02.1080p.mkv", "555555555", "magnet:S01E02.1080p.mkv", "url://S01E02.1080p.mkv", 55, 555, 5555),
+ TorrentFile("S02E01.720p.mkv", "666666666", "magnet:S02E01.720p.mkv", "url://S02E01.720p.mkv", 66, 666, 6666),
+)
+
+private val episodes = listOf(
+ Episode(1, 1).apply {
+ addTorrentFile(torrentFiles[0])
+ addTorrentFile(torrentFiles[1])
+ addTorrentFile(torrentFiles[2])
+ },
+ Episode(1, 2).apply {
+ addTorrentFile(torrentFiles[3])
+ addTorrentFile(torrentFiles[4])
+ },
+ Episode(2, 1).apply {
+ addTorrentFile(torrentFiles[5])
+ }
+)
--- /dev/null
+package net.pterodactylus.util.test
+
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeDiagnosingMatcher
+import org.w3c.dom.Node
+
+/**
+ * Returns a Hamcrest [matcher][org.hamcrest.Matcher] that matches
+ * [nodes][Node] with a name of “a”, a “href” attribute matching the given
+ * [link], and a text matching the given [text].
+ *
+ * @param[text] The text of the link node
+ * @param[link] The link of the link node
+ * @return A [matcher][org.hamcrest.Matcher] for [nodes][Node]
+ */
+fun isLinkNode(text: String, link: String) = object : TypeSafeDiagnosingMatcher<Node>() {
+ override fun matchesSafely(node: Node, mismatchDescription: Description) =
+ if (node.nodeType != Node.ELEMENT_NODE) {
+ mismatchDescription.appendText("is not link node")
+ false
+ } else if (node.textContent != text) {
+ mismatchDescription.appendText("text is ").appendValue(node.textContent)
+ false
+ } else if (node.attributes.getNamedItem("href").nodeValue != link) {
+ mismatchDescription.appendText("link is ").appendValue(node.attributes.getNamedItem("href").nodeValue)
+ false
+ } else {
+ true
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("link node with text ").appendValue(text).appendText(" and link ").appendValue(link)
+ }
+}
+
+/**
+ * Returns a Hamcrest [matcher][org.hamcrest.Matcher] for [nodes][Node] that
+ * either are text nodes with the given [text], or that are element nodes whose
+ * [textContent][Node.getTextContent] is the given [text].
+ *
+ * @param[text] The text to match
+ * @return A [matcher][org.hamcrest.Matcher] for [nodes][Node]
+ */
+fun isTextNode(text: String) = object : TypeSafeDiagnosingMatcher<Node>() {
+ override fun matchesSafely(node: Node, mismatchDescription: Description) =
+ if ((node.nodeType != Node.TEXT_NODE) && (node.nodeType != Node.ELEMENT_NODE)) {
+ mismatchDescription.appendText("is ").appendValue(node.nodeType).appendText(" node")
+ false;
+ } else if ((node.nodeType == Node.TEXT_NODE) && (node.nodeValue != text)) {
+ mismatchDescription.appendText("text is ").appendValue(node.nodeValue)
+ false
+ } else if (node.textContent != text) {
+ mismatchDescription.appendText("text content is ").appendValue(node.textContent)
+ false
+ } else {
+ true
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("text node with text ").appendValue(text)
+ }
+}