gemini-server.t 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # Copyright (C) 2017–2020 Alex Schroeder <alex@gnu.org>
  2. #
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. package OddMuse;
  16. use strict;
  17. use 5.10.0;
  18. use Test::More;
  19. use IO::Socket::SSL;
  20. use utf8; # tests contain UTF-8 characters and it matters
  21. use Modern::Perl;
  22. use XML::RSS;
  23. use XML::LibXML;
  24. require './t/test.pl';
  25. require './stuff/gemini-server.pl';
  26. add_module('tags.pl');
  27. # enable uploads and filtering by language
  28. our($ConfigFile);
  29. AppendStringToFile($ConfigFile, <<'EOT');
  30. $UploadAllowed = 1;
  31. %Languages = (
  32. 'de' => '\b(der|die|das|und|oder)\b',
  33. 'en' => '\b(i|he|she|it|we|they|this|that|a|is|was)\b', );
  34. EOT
  35. # enable comments
  36. our($CommentsPrefix);
  37. $CommentsPrefix = 'Comments_on_';
  38. AppendStringToFile($ConfigFile, "\$CommentsPrefix = 'Comments_on_';\n");
  39. AppendStringToFile($ConfigFile, "\@QuestionaskerQuestions = (['Who rules in Rivendell?' => sub { shift =~ /^Elrond/i }]);\n");
  40. # write a gemini-only extension
  41. our($DataDir);
  42. WriteStringToFile("$DataDir/gemini_config", <<'EOT');
  43. package OddMuse;
  44. use Modern::Perl;
  45. our (@extensions, @main_menu_links);
  46. push(@extensions, \&serve_cert);
  47. sub serve_cert {
  48. my $self = shift;
  49. my $url = shift;
  50. my $selector = shift;
  51. my $base = $self->base();
  52. if ($selector =~ m!^do/test!) {
  53. say "20 text/plain\r";
  54. say "Test";
  55. return 1;
  56. }
  57. return;
  58. }
  59. 1;
  60. EOT
  61. my $host = "127.0.0.1";
  62. my $port = random_port();
  63. my $pid = fork();
  64. END {
  65. # kill server
  66. if ($pid) {
  67. kill 'KILL', $pid or warn "Could not kill server $pid";
  68. }
  69. }
  70. if (!defined $pid) {
  71. die "Cannot fork: $!";
  72. } elsif ($pid == 0) {
  73. use Config;
  74. my $secure_perl_path = $Config{perlpath};
  75. exec($secure_perl_path,
  76. "stuff/gemini-server.pl",
  77. "--host=$host",
  78. "--port=$port",
  79. "--wiki_cert_file=t/cert.pem",
  80. "--wiki_key_file=t/key.pem",
  81. "--log_level=0", # set to 4 for verbose logging
  82. "--wiki=./wiki.pl",
  83. "--wiki_dir=$DataDir",
  84. "--wiki_pages=Alex",
  85. "--wiki_pages=Berta",
  86. "--wiki_pages=Chris")
  87. or die "Cannot exec: $!";
  88. }
  89. # Sorting
  90. is(sub{$a="Alex"; $b="Berta"; newest_first()}->(), -1, "Alex before Berta");
  91. is(sub{$a="Alex"; $b="Comments_on_Alex"; newest_first()}->(), -1, "Alex before Comments_on_Alex");
  92. is(sub{$a="Chris"; $b="Comments_on_Alex"; newest_first()}->(), 1, "Chris after Comments_on_A");
  93. is(sub{$a="Image_1_for_Alex"; $b="Image_10_for_Alex"; newest_first()}->(), -1, "Image_1_for_Alex before Image_10_for_Alex");
  94. is(sub{$a="Comments_on_Alex"; $b="Image_1_for_Alex"; newest_first()}->(), -1, "Comments_on_Alex before Image_1_for_Alex");
  95. is(join(" ", sort newest_first qw(Alex Berta Chris)), "Alex Berta Chris", "Sort alphabetically");
  96. is(join(" ", sort newest_first qw(2017-12-25 2017-12-26 2017-12-27)), "2017-12-27 2017-12-26 2017-12-25", "Sort by date descending");
  97. is(join(" ", sort newest_first qw(Alex Comments_on_Alex Berta Chris)), "Alex Comments_on_Alex Berta Chris", "Comments after pages");
  98. is(join(" ", sort newest_first qw(2017-12-25 2017-12-26 Comments_on_2017-12-26 2017-12-27)), "2017-12-27 2017-12-26 Comments_on_2017-12-26 2017-12-25", "Comments after date pages");
  99. is(join(" ", sort newest_first qw(Alex Comments_on_Alex Image_1_for_Alex Image_2_for_Alex Image_10_for_Alex Berta Chris)), "Alex Comments_on_Alex Image_1_for_Alex Image_2_for_Alex Image_10_for_Alex Berta Chris", "Images sorted numerically");
  100. update_page('Alex', "My best friend is [[Berta]].\n\nTags: [[tag:Friends]]\n");
  101. update_page('Berta', "This is me.\n\nTags: [[tag:Friends]]\n");
  102. update_page('Chris', "I'm Chris.\n\nTags: [[tag:Friends]]\n");
  103. update_page('Friends', "Some friends.\n");
  104. update_page('2017-12-25', 'It was a Monday.\n\nTags: [[tag:Day]]');
  105. update_page('2017-12-26', 'It was a Tuesday.\n\nTags: [[tag:Day]]');
  106. update_page('2017-12-27', 'It was a Wednesday.\n\nTags: [[tag:Day]]');
  107. update_page('Friends', "News about friends.\n", 'rewrite', 1); # minor change
  108. update_page('Friends', "News about friends:\n\n<journal search tag:friends>\n",
  109. 'add journal tag', 1); # minor change
  110. # file created using convert NULL: test.png && base64 test.png
  111. update_page('Picture',
  112. "#FILE image/png\niVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bv"
  113. . "kkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg==");
  114. sub query_gemini {
  115. my $query = shift;
  116. my $text = shift;
  117. # create client
  118. my $socket = IO::Socket::SSL->new(
  119. PeerHost => "localhost",
  120. PeerService => $port,
  121. SSL_cert_file => 'cert.pem',
  122. SSL_key_file => 'key.pem',
  123. SSL_verify_mode => SSL_VERIFY_NONE)
  124. or die "Cannot construct client socket: $@";
  125. $socket->print("$query\r\n");
  126. $socket->print($text);
  127. undef $/; # slurp
  128. return <$socket>;
  129. }
  130. my $base = "gemini://$host:$port";
  131. # main menu
  132. my $page = query_gemini("$base/");
  133. for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
  134. like($page, qr/^=> $base\/$item $item/m, "main menu contains $item");
  135. }
  136. unlike($page, qr/^=> .*\/$/m, "No empty links in the menu");
  137. $page = query_gemini("$base/Alex");
  138. like($page, qr/^My best friend is Berta\.$/m, "Local free link (text)");
  139. like($page, qr/=> $base\/Berta Berta$/m, "Local free link (link)");
  140. like($page, qr/^Tags:$/m, "Tags footer");
  141. like($page, qr/^Tags:$/m, "Tags footer");
  142. like($page, qr/=> $base\/tag\/Friends Friends$/m, "Tag link");
  143. like($page, qr/^=> $base\/raw\/Alex Raw text$/m, "Raw text link");
  144. like($page, qr/^=> $base\/history\/Alex History$/m, "History");
  145. like($page, qr/^=> $base\/Comments_on_Alex Comments on this page$/m, "Comment link");
  146. # language tag
  147. $page = query_gemini("$base\/2017-12-25");
  148. like($page, qr/^20 text\/gemini; charset=UTF-8; lang=en\r\n/, "Result 20 with MIME type and language");
  149. # plain text
  150. $page = query_gemini("$base\/raw\/Alex");
  151. like($page, qr/^My best friend is \[\[Berta\]\]\.$/m, "Raw text");
  152. # history
  153. $page = query_gemini("$base/history/Friends");
  154. like($page, qr/^=> $base\/Friends\/1 Friends \(1\)/m, "Revision 1 is listed");
  155. like($page, qr/^=> $base\/Friends\/2 Friends \(2\)/m, "Revision 2 is listed");
  156. like($page, qr/^=> $base\/diff\/Friends\/1 Diff between revision 1 and the current one/m, "Diff 1 link");
  157. like($page, qr/^=> $base\/diff\/Friends\/2 Diff between revision 2 and the current one/m, "Diff 2 link");
  158. like($page, qr/^=> $base\/Friends Friends \(current\)/m, "Current revision is listed");
  159. $page = query_gemini("$base/Friends/1");
  160. like($page, qr/^Some friends\.$/m, "Revision 1 content");
  161. $page = query_gemini("$base/Friends/2");
  162. like($page, qr/^News about friends\.$/m, "Revision 2 content");
  163. #diffs
  164. $page = query_gemini("$base/diff/Friends/1");
  165. like($page, qr/^< Some friends\.\n-+\n> News about friends:\n> \n> <journal search tag:friends>\n$/m, "Diff 1 content");
  166. $page = query_gemini("$base/diff/Friends/2");
  167. like($page, qr/^< News about friends\.\n-+\n> News about friends:\n> \n> <journal search tag:friends>\n$/m, "Diff 1 content");
  168. # tags
  169. $page = query_gemini("$base\/tag\/Friends");
  170. like($page, qr/^This page is about the tag Friends\.$/m, "tag menu intro");
  171. for my $item(qw(Friends Alex Berta Chris)) {
  172. like($page, qr/^=> $base\/$item $item$/m, "tag menu contains $item");
  173. }
  174. # tags
  175. $page = query_gemini("$base\/tag\/Day");
  176. like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
  177. "tag menu sorted newest first");
  178. # match
  179. $page = query_gemini("$base\/do/match?2017");
  180. for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
  181. like($page, qr/^=> $base\/$item $item$/m, "match menu contains $item");
  182. }
  183. like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
  184. "match menu sorted newest first");
  185. # search
  186. $page = query_gemini("$base\/do/search?tag:day");
  187. for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
  188. like($page, qr/^=> $base\/$item $item/m, "search menu contains $item");
  189. }
  190. like($page, qr/2017-12-27.*2017-12-26.*2017-12-25/s,
  191. "search menu sorted newest first");
  192. # rc
  193. $page = query_gemini("$base\/do/rc");
  194. my $re = join(".*", "Picture", "2017-12-27", "2017-12-26", "2017-12-25",
  195. "Friends", "Chris", "Berta", "Alex");
  196. like($page, qr/$re/s, "rc in the right order");
  197. $page = query_gemini("$base\/do/rc/minor");
  198. $re = join(".*", "Friends", "2017-12-27", "2017-12-26", "2017-12-25");
  199. like($page, qr/$re/s, "minor rc in the right order");
  200. # feeds
  201. my $xpc = XML::LibXML::XPathContext->new;
  202. $xpc->registerNs('atom', 'http://www.w3.org/2005/Atom');
  203. # rss with regular pages
  204. my $feed = new XML::RSS;
  205. $page = query_gemini("$base\/do/rss");
  206. ok($page =~ s!^20 application/rss\+xml\r\n!!, "RSS header OK");
  207. ok($feed->parse($page), "RSS parse OK");
  208. for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
  209. ok(grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item found in RSS feed");
  210. }
  211. # atom with regular pages
  212. $page = query_gemini("$base\/do/atom");
  213. ok($page =~ s!^20 application/atom\+xml\r\n!!, "Atom header OK");
  214. # $feed->parse($page) results in warnings that I can't get rid of
  215. ok(my $doc = XML::LibXML->load_xml(string => $page), "Atom parse OK");
  216. for my $item(qw(Alex Berta Chris 2017-12-25 2017-12-26 2017-12-27)) {
  217. ok($xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item found in Atom feed");
  218. }
  219. add_module('journal-rss.pl');
  220. # rss with just the journal
  221. $page = query_gemini("$base\/do/rss");
  222. ok($page =~ s!^20 application/rss\+xml\r\n!!, "RSS header OK");
  223. ok($feed->parse($page), "RSS parse OK");
  224. for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
  225. ok(grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item found in RSS feed");
  226. }
  227. for my $item(qw(Alex Berta Chris)) {
  228. ok(!grep(/$item/, map { $_->{title} } @{$feed->{items}}), "$item not found in RSS feed");
  229. }
  230. my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime;
  231. $year += 1900;
  232. # Fri, 19 Jun 2020 20:41:55 GMT
  233. my $today = sprintf("%s, %02d %s %d",
  234. qw(Sun Mon Tue Wed Thu Fri Sat)[$wday], $mday,
  235. qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon], $year);
  236. like($page, qr!<pubDate>$today \d\d:\d\d:\d\d GMT</pubDate>!, "Update timestamp for today");
  237. # atom with just the journal
  238. $page = query_gemini("$base\/do/atom");
  239. ok($page =~ s!^20 application/atom\+xml\r\n!!, "Atom header OK");
  240. # $feed->parse($page) results in warnings that I can't get rid of
  241. ok($doc = XML::LibXML->load_xml(string => $page), "Atom parse OK");
  242. for my $item(qw(2017-12-25 2017-12-26 2017-12-27)) {
  243. ok($xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item found in Atom feed");
  244. }
  245. for my $item(qw(Alex Berta Chris)) {
  246. ok(!$xpc->find("//atom:entry/atom:title[text()='$item']", $doc), "$item not found in Atom feed");
  247. }
  248. $today = sprintf("%d-%02d-%02d", $year, $mon+1, $mday);
  249. like($page, qr!<updated>${today}T\d\d:\d\d:\d\dZ</updated>!, "Update timestamp for today");
  250. # upload text
  251. my $titan = "titan://$host:$port";
  252. my $haiku = <<EOT;
  253. Quiet disk ratling
  254. Keyboard clicking, then it stops.
  255. Rain falls and I think
  256. EOT
  257. $page = query_gemini("$titan/raw/Haiku;size=76;mime=text/plain", $haiku);
  258. like($page, qr/^30 $base\/Haiku\r$/, "Titan Haiku");
  259. my $haiku_re = $haiku;
  260. $haiku_re =~ s/\s+/ /g; # lines get wrapped
  261. $haiku_re =~ s/\s+$//g;
  262. $haiku_re = quotemeta($haiku_re);
  263. $page = query_gemini("$base/Haiku");
  264. like($page, qr/^$haiku_re/m, "Haiku saved");
  265. # comment
  266. like($page, qr/^=> $base\/Comments_on_Haiku Comments on this page$/m, "Comment page link");
  267. $page = query_gemini("$base/Comments_on_Haiku");
  268. like($page, qr/^=> $base\/do\/comment\/Comments_on_Haiku Leave a comment$/m, "Leave comment link");
  269. $page = query_gemini("$base/do/comment/Comments_on_Haiku");
  270. like($page, qr/^30 $base\/do\/comment\/Comments_on_Haiku\/0\r$/, "Redirect to a question");
  271. $page = query_gemini("$base/do/comment/Comments_on_Haiku/0");
  272. like($page, qr/^10 Who rules in Rivendell\?\r$/, "Ask security question");
  273. $page = query_gemini("$base/do/comment/Comments_on_Haiku/0?elrond");
  274. like($page, qr/^30 $base\/do\/comment\/Comments_on_Haiku\/0\/elrond\r$/, "Redirect to comment prompt");
  275. $page = query_gemini("$base/do/comment/Comments_on_Haiku/0/elrond");
  276. like($page, qr/^10 Comment\r$/, "Ask for comment");
  277. $page = query_gemini("$base/do/comment/Comments_on_Haiku/0/elrond?Give%20me%20the%20ring!");
  278. like($page, qr/^30 $base\/Comments_on_Haiku\r$/, "Redirect back to the main page");
  279. $page = query_gemini("$base/Comments_on_Haiku");
  280. like($page, qr/^Give me the ring!\n\n-- Anonymous/m, "Comment saved");
  281. # extension
  282. $page = query_gemini("$base/do/test");
  283. like($page, qr/^Test\n/m, "Extension runs");
  284. done_testing();