🐛 Fix breaking parser on invalid SSK/USKs
[Sone.git] / src / test / kotlin / net / pterodactylus / sone / text / SoneTextParserTest.kt
1 /*
2  * Sone - SoneTextParserTest.kt - Copyright © 2011–2020 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.text
19
20 import com.google.inject.Guice.*
21 import freenet.keys.FreenetURI
22 import net.pterodactylus.sone.data.*
23 import net.pterodactylus.sone.data.impl.*
24 import net.pterodactylus.sone.database.*
25 import net.pterodactylus.sone.test.*
26 import org.hamcrest.MatcherAssert.*
27 import org.hamcrest.Matchers.*
28 import kotlin.test.*
29
30 /**
31  * JUnit test case for [SoneTextParser].
32  */
33 class SoneTextParserTest {
34
35         private val soneTextParser = SoneTextParser(null, null)
36
37         @Test
38         fun `basic operation`() {
39                 val parts = soneTextParser.parse("Test.", null)
40                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java), equalTo("Test."))
41         }
42
43         @Test
44         fun `empty lines at start and end are stripped`() {
45                 val parts = soneTextParser.parse("\nTest.\n\n", null)
46                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java), equalTo("Test."))
47         }
48
49         @Test
50         fun `duplicate empty lines in the text are stripped`() {
51                 val parts = soneTextParser.parse("\nTest.\n\n\nTest.", null)
52                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java), equalTo("Test.\n\nTest."))
53         }
54
55         @Test
56         fun `consecutive lines are separated by linefeed`() {
57                 val parts = soneTextParser.parse("Text.\nText", null)
58                 assertThat("Part Text", convertText(parts), equalTo("Text.\nText"))
59         }
60
61         @Test
62         fun `freenet links have the freenet prefix removed`() {
63                 val parts = soneTextParser.parse("freenet:KSK@gpl.txt", null)
64                 assertThat("Part Text", convertText(parts), equalTo("[KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"))
65         }
66
67         @Test
68         fun `only the first item in a line is prefixed with a line break`() {
69                 val parts = soneTextParser.parse("Text.\nKSK@gpl.txt and KSK@gpl.txt", null)
70                 assertThat("Part Text", convertText(parts), equalTo("Text.\n[KSK@gpl.txt|KSK@gpl.txt|gpl.txt] and [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"))
71         }
72
73         @Test
74         fun `sone link with too short sone ID is rendered as plain text`() {
75                 val parts = soneTextParser.parse("sone://too-short", null)
76                 assertThat("Part Text", convertText(parts), equalTo("sone://too-short"))
77         }
78
79         @Test
80         fun `sone link is rendered correctly if sone is not present`() {
81                 val parser = SoneTextParser(AbsentSoneProvider(), null)
82                 val parts = parser.parse("sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU", null)
83                 assertThat("Part Text", convertText(parts), equalTo("[Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU]"))
84         }
85
86         @Test
87         fun `sone and post can be parsed from the same text`() {
88                 val parser = SoneTextParser(TestSoneProvider(), TestPostProvider())
89                 val parts = parser.parse("Text sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU more text post://f3757817-b45a-497a-803f-9c5aafc10dc6 even more text", null)
90                 assertThat("Part Text", convertText(parts), equalTo("Text [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] more text [Post|f3757817-b45a-497a-803f-9c5aafc10dc6|text] even more text"))
91         }
92
93         @Test
94         fun `post link is rendered as plain text if post ID is too short`() {
95                 val parts = soneTextParser.parse("post://too-short", null)
96                 assertThat("Part Text", convertText(parts), equalTo("post://too-short"))
97         }
98
99         @Test
100         fun `post link is rendered correctly if post is present`() {
101                 val parser = SoneTextParser(null, TestPostProvider())
102                 val parts = parser.parse("post://f3757817-b45a-497a-803f-9c5aafc10dc6", null)
103                 assertThat("Part Text", convertText(parts), equalTo("[Post|f3757817-b45a-497a-803f-9c5aafc10dc6|text]"))
104         }
105
106         @Test
107         fun `post link is rendered as plain text if post is absent`() {
108                 val parser = SoneTextParser(null, AbsentPostProvider())
109                 val parts = parser.parse("post://f3757817-b45a-497a-803f-9c5aafc10dc6", null)
110                 assertThat("Part Text", convertText(parts), equalTo("post://f3757817-b45a-497a-803f-9c5aafc10dc6"))
111         }
112
113         @Test
114         fun `name of freenet link does not contain url parameters`() {
115                 val parts = soneTextParser.parse("KSK@gpl.txt?max-size=12345", null)
116                 assertThat("Part Text", convertText(parts), equalTo("[KSK@gpl.txt?max-size=12345|KSK@gpl.txt|gpl.txt]"))
117         }
118
119         @Test
120         fun `trailing slash in freenet link is removed for name`() {
121                 val parts = soneTextParser.parse("KSK@gpl.txt/", null)
122                 assertThat("Part Text", convertText(parts), equalTo("[KSK@gpl.txt/|KSK@gpl.txt/|gpl.txt]"))
123         }
124
125         @Test
126         fun `last meta string of freenet link is used as name`() {
127                 val parts = soneTextParser.parse("CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING", null)
128                 assertThat("Part Text", convertText(parts), equalTo("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING|CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/COPYING|COPYING]"))
129         }
130
131         @Test
132         fun `freenet link without meta strings and doc name gets first nine characters of key as name`() {
133                 val parts = soneTextParser.parse("CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8", null)
134                 assertThat("Part Text", convertText(parts), equalTo("[CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|CHK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|CHK@qM1nm]"))
135         }
136
137         @Test
138         fun `malformed key is rendered as plain text`() {
139                 val parts = soneTextParser.parse("CHK@qM1nmgU", null)
140                 assertThat("Part Text", convertText(parts), equalTo("CHK@qM1nmgU"))
141         }
142
143         @Test
144         fun `https link has its paths shortened`() {
145                 val parts = soneTextParser.parse("https://test.test/some-long-path/file.txt", null)
146                 assertThat("Part Text", convertText(parts), equalTo("[https://test.test/some-long-path/file.txt|https://test.test/some-long-path/file.txt|test.test/…/file.txt]"))
147         }
148
149         @Test
150         fun `http links have their last slash removed`() {
151                 val parts = soneTextParser.parse("http://test.test/test/", null)
152                 assertThat("Part Text", convertText(parts), equalTo("[http://test.test/test/|http://test.test/test/|test.test/…]"))
153         }
154
155         @Test
156         fun `www prefix is removed for hostname with two dots and no path`() {
157                 val parts = soneTextParser.parse("http://www.test.test", null)
158                 assertThat("Part Text", convertText(parts), equalTo("[http://www.test.test|http://www.test.test|test.test]"))
159         }
160
161         @Test
162         fun `www prefix is removed for hostname with two dots and a path`() {
163                 val parts = soneTextParser.parse("http://www.test.test/test.html", null)
164                 assertThat("Part Text", convertText(parts), equalTo("[http://www.test.test/test.html|http://www.test.test/test.html|test.test/test.html]"))
165         }
166
167         @Test
168         fun `hostname is kept intact if not beginning with www`() {
169                 val parts = soneTextParser.parse("http://test.test.test/test.html", null)
170                 assertThat("Part Text", convertText(parts), equalTo("[http://test.test.test/test.html|http://test.test.test/test.html|test.test.test/test.html]"))
171         }
172
173         @Test
174         fun `hostname with one dot but no slash is kept intact`() {
175                 val parts = soneTextParser.parse("http://test.test", null)
176                 assertThat("Part Text", convertText(parts), equalTo("[http://test.test|http://test.test|test.test]"))
177         }
178
179         @Test
180         fun `url parameters are removed for http links`() {
181                 val parts = soneTextParser.parse("http://test.test?foo=bar", null)
182                 assertThat("Part Text", convertText(parts), equalTo("[http://test.test?foo=bar|http://test.test?foo=bar|test.test]"))
183         }
184
185         @Test
186         fun `empty string is parsed correctly`() {
187                 val parts = soneTextParser.parse("", null)
188                 assertThat("Part Text", convertText(parts), equalTo(""))
189         }
190
191         @Test
192         fun `links are parsed in correct order`() {
193                 val parts = soneTextParser.parse("KSK@ CHK@", null)
194                 assertThat("Part Text", convertText(parts), equalTo("KSK@ CHK@"))
195         }
196
197         @Test
198         fun `invalid ssk and usk link is parsed as text`() {
199                 val parts = soneTextParser.parse("SSK@a USK@a", null)
200                 assertThat("Part Text", convertText(parts), equalTo("SSK@a USK@a"))
201         }
202
203         @Test
204         fun `ssk without document name is parsed correctly`() {
205                 val parts = soneTextParser.parse(
206                                 "SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8", null)
207                 assertThat("Part Text", convertText(parts),
208                                 equalTo("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|"
209                                                 + "SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8|"
210                                                 + "SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU]"))
211         }
212
213         @Test
214         fun `ssk link without context is not trusted`() {
215                 val parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", null)
216                 assertThat("Part Text", convertText(parts), equalTo("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"))
217         }
218
219         @Test
220         fun `ssk link with context without sone is not trusted`() {
221                 val context = SoneTextParserContext(null)
222                 val parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context)
223                 assertThat("Part Text", convertText(parts), equalTo("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"))
224         }
225
226         @Test
227         fun `ssk link with context with different sone is not trusted`() {
228                 val context = SoneTextParserContext(IdOnlySone("DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU"))
229                 val parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context)
230                 assertThat("Part Text", convertText(parts), equalTo("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"))
231         }
232
233         @Test
234         fun `ssk link with context with correct sone is trusted`() {
235                 val context = SoneTextParserContext(IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"))
236                 val parts = soneTextParser.parse("SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test", context)
237                 assertThat("Part Text", convertText(parts), equalTo("[SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|trusted|SSK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test|test]"))
238         }
239
240         @Test
241         fun `usk link with context with correct sone is trusted`() {
242                 val context = SoneTextParserContext(IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"))
243                 val parts = soneTextParser.parse("USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0", context)
244                 assertThat("Part Text", convertText(parts), equalTo("[USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|trusted|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]"))
245         }
246
247         @Test
248         fun `usk links with backlinks is parsed correctly`() {
249                 val context = SoneTextParserContext(IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"))
250                 val parts = soneTextParser.parse("USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0/../../../USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/78/", context)
251                 assertThat("Part Text", convertText(parts), equalTo("[USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|trusted|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]"))
252         }
253
254         @Test
255         fun `broken usk links is parsed as plain text`() {
256                 val context = SoneTextParserContext(IdOnlySone("qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU"))
257                 val parts = soneTextParser.parse("USK@/someCrazyName.R1/0", context)
258                 FreenetURI("USK@/someCrazyName.R1/0")
259                 assertThat("Part Text", convertText(parts), equalTo("USK@/someCrazyName.R1/0"))
260         }
261
262         @Test
263         fun `test basic ksk links`() {
264                 val parts: Iterable<Part> = soneTextParser.parse("KSK@gpl.txt", null)
265                 assertThat("Part Text", convertText(parts, FreenetLinkPart::class.java), equalTo("[KSK@gpl.txt|KSK@gpl.txt|gpl.txt]"))
266         }
267
268         @Test
269         fun `embedded ksk links are parsed correctly`() {
270                 val parts = soneTextParser.parse("Link is KSK@gpl.txt\u200b.", null)
271                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, FreenetLinkPart::class.java), equalTo("Link is [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]\u200b."))
272         }
273
274         @Test
275         fun `embedded ksk links and line breaks are parsed correctly`() {
276                 val parts = soneTextParser.parse("Link is KSK@gpl.txt\nKSK@test.dat\n", null)
277                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, FreenetLinkPart::class.java), equalTo("Link is [KSK@gpl.txt|KSK@gpl.txt|gpl.txt]\n[KSK@test.dat|KSK@test.dat|test.dat]"))
278         }
279
280         @Test
281         fun `ksk links with backlinks are parsed correctly`() {
282                 val parts = soneTextParser.parse("KSK@gallery/../Sone/imageBrowser.html?album=30c930ee-97cd-11e9-bd44-f3e595768b77", null)
283                 assertThat("Part Text", convertText(parts, FreenetLinkPart::class.java), equalTo("[KSK@gallery|KSK@gallery|gallery]"))
284         }
285
286         @Test
287         fun `test empty lines and sone links`() {
288                 val soneTextParser = SoneTextParser(TestSoneProvider(), null)
289
290                 /* check basic links. */
291                 val parts = soneTextParser.parse("Some text.\n\nLink to sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU and stuff.", null)
292                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, SonePart::class.java), equalTo("Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff."))
293         }
294
295         @Test
296         fun `test empy http links`() {
297                 val soneTextParser = SoneTextParser(TestSoneProvider(), null)
298
299                 /* check empty http links. */
300                 val parts = soneTextParser.parse("Some text. Empty link: http:// – nice!", null)
301                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java), equalTo("Some text. Empty link: http:// – nice!"))
302         }
303
304         @Test
305         fun `http link without parens ends at next closing paren`() {
306                 val parts = soneTextParser.parse("Some text (and a link: http://example.sone/abc) – nice!", null)
307                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("Some text (and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]) – nice!"))
308         }
309
310         @Test
311         fun `usk link ends at first non numeric non slash character after version number`() {
312                 val parts = soneTextParser.parse("Some link (USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0). Nice", null)
313                 assertThat("Part Text", convertText(parts), equalTo("Some link ([USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]). Nice"))
314         }
315
316         @Test
317         fun `usk link with filename shows the filename`() {
318                 val parts = soneTextParser.parse("Some link (USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0/images/image.jpg). Nice", null)
319                 assertThat("Part Text", convertText(parts), equalTo("Some link ([USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0/images/image.jpg|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0/images/image.jpg|image.jpg]). Nice"))
320         }
321
322         @Test
323         fun `usk link without filename but ending in slash shows the path`() {
324                 val parts = soneTextParser.parse("Some link (USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0/). Nice", null)
325                 assertThat("Part Text", convertText(parts), equalTo("Some link ([USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|USK@qM1nmgU-YUnIttmEhqjTl7ifAF3Z6o~5EPwQW03uEQU,aztSUkT-VT1dWvfSUt9YpfyW~Flmf5yXpBnIE~v8sAg,AAMC--8/test/0|test]). Nice"))
326         }
327
328         @Test
329         fun `http link with opened and closed parens ends at next closing paren`() {
330                 val parts = soneTextParser.parse("Some text (and a link: http://example.sone/abc_(def)) – nice!", null)
331                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("Some text (and a link: [http://example.sone/abc_(def)|http://example.sone/abc_(def)|example.sone/abc_(def)]) – nice!"))
332         }
333
334         @Test
335         fun `punctuation is ignored at end of link before whitespace`() {
336                 val parts = soneTextParser.parse("Some text and a link: http://example.sone/abc. Nice!", null)
337                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]. Nice!"))
338         }
339
340         @Test
341         fun `multiple punctuation characters are ignored at end of link before whitespace`() {
342                 val parts = soneTextParser.parse("Some text and a link: http://example.sone/abc... Nice!", null)
343                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]... Nice!"))
344         }
345
346         @Test
347         fun `commas are ignored at end of link before whitespace`() {
348                 val parts = soneTextParser.parse("Some text and a link: http://example.sone/abc, nice!", null)
349                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("Some text and a link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc], nice!"))
350         }
351
352         @Test
353         fun `exclamation marks are ignored at end of link before whitespace`() {
354                 val parts = soneTextParser.parse("A link: http://example.sone/abc!", null)
355                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("A link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]!"))
356         }
357
358         @Test
359         fun `question marks are ignored at end of link before whitespace`() {
360                 val parts = soneTextParser.parse("A link: http://example.sone/abc?", null)
361                 assertThat("Part Text", convertText(parts, PlainTextPart::class.java, LinkPart::class.java), equalTo("A link: [http://example.sone/abc|http://example.sone/abc|example.sone/abc]?"))
362         }
363
364         @Test
365         fun `correct freemail address is linked to correctly`() {
366                 val parts = soneTextParser.parse("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null)
367                 assertThat("Part Text", convertText(parts), equalTo("Mail me at [Freemail|sone|t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra|nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI]!"))
368         }
369
370         @Test
371         fun `freemail address with invalid freemail id is parsed as text`() {
372                 val parts = soneTextParser.parse("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqr8.freemail!", null)
373                 assertThat("Part Text", convertText(parts), equalTo("Mail me at sone@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqr8.freemail!"))
374         }
375
376         @Test
377         fun `freemail address with invalid sized freemail id is parsed as text`() {
378                 val parts = soneTextParser.parse("Mail me at sone@4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null)
379                 assertThat("Part Text", convertText(parts), equalTo("Mail me at sone@4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!"))
380         }
381
382         @Test
383         fun `freemail address without local part is parsed as text`() {
384                 val parts = soneTextParser.parse("     @t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!", null)
385                 assertThat("Part Text", convertText(parts), equalTo("     @t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail!"))
386         }
387
388         @Test
389         fun `local part of freemail address can contain letters digits minus dot underscore`() {
390                 val parts = soneTextParser.parse("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._@t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra.freemail", null)
391                 assertThat("Part Text", convertText(parts), equalTo("[Freemail|ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._|t4dlzfdww3xvsnsc6j6gtliox6zaoak7ymkobbmcmdw527ubuqra|nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI]"))
392         }
393
394         private fun convertText(parts: Iterable<Part>, vararg validClasses: Class<*>): String {
395                 if (validClasses.isNotEmpty()) {
396                         assertThat(parts.map { it.javaClass }.distinct() - validClasses.distinct(), empty())
397                 }
398                 return parts.joinToString("") { part ->
399                         when (part) {
400                                 is PlainTextPart -> part.text
401                                 is FreenetLinkPart -> "[${part.link}|${if (part.trusted) "trusted|" else ""}${part.title}|${part.text}]"
402                                 is FreemailPart -> "[Freemail|${part.emailLocalPart}|${part.freemailId}|${part.identityId}]"
403                                 is LinkPart -> "[${part.link}|${part.title}|${part.text}]"
404                                 is SonePart -> "[Sone|${part.sone.id}]"
405                                 is PostPart -> "[Post|${part.post.id}|${part.post.text}]"
406                                 else -> throw NoSuchElementException()
407                         }
408                 }
409         }
410
411         @Test
412         fun `parser can be created by guice`() {
413                 val injector = createInjector(
414                                 SoneProvider::class.isProvidedByMock(),
415                                 PostProvider::class.isProvidedByMock()
416                 )
417                 assertThat(injector.getInstance<SoneTextParser>(), notNullValue())
418         }
419
420         /**
421          * Mock Sone provider.
422          */
423         private open class TestSoneProvider : SoneProvider {
424
425                 override val soneLoader = this::getSone
426                 override val sones: Collection<Sone> = emptySet()
427                 override val localSones: Collection<Sone> = emptySet()
428                 override val remoteSones: Collection<Sone> = emptySet()
429
430                 override fun getSone(soneId: String): Sone? = IdOnlySone(soneId)
431
432         }
433
434         private class AbsentSoneProvider : TestSoneProvider() {
435
436                 override fun getSone(soneId: String): Sone? = null
437
438         }
439
440         private open class TestPostProvider : PostProvider {
441
442                 override fun getPost(postId: String): Post? {
443                         return object : Post {
444                                 override val id = postId
445                                 override fun isLoaded() = false
446                                 override fun getSone() = null
447                                 override fun getRecipientId() = null
448                                 override fun getRecipient() = null
449                                 override fun getTime() = 0L
450                                 override fun getText() = "text"
451                                 override fun isKnown() = false
452                                 override fun setKnown(known: Boolean) = null
453                         }
454                 }
455
456                 override fun getPosts(soneId: String) = emptySet<Post>()
457                 override fun getDirectedPosts(recipientId: String) = emptySet<Post>()
458
459         }
460
461         private class AbsentPostProvider : TestPostProvider() {
462
463                 override fun getPost(postId: String): Post? = null
464
465         }
466
467 }