]> Sergey Matveev's repositories - public-inbox.git/blob - t/nntpd.t
nntp: implement OVER/XOVER summary in search document
[public-inbox.git] / t / nntpd.t
1 # Copyright (C) 2015 all contributors <meta@public-inbox.org>
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3 use strict;
4 use warnings;
5 use Test::More;
6 eval { require PublicInbox::SearchIdx };
7 plan skip_all => "Xapian missing for nntpd" if $@;
8 eval { require PublicInbox::Msgmap };
9 plan skip_all => "DBD::SQLite missing for nntpd" if $@;
10 use Cwd;
11 use Email::Simple;
12 use IO::Socket;
13 use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD);
14 use Socket qw(SO_KEEPALIVE IPPROTO_TCP TCP_NODELAY);
15 use File::Temp qw/tempdir/;
16 use Net::NNTP;
17 use IPC::Run qw(run);
18 use Data::Dumper;
19
20 my $tmpdir = tempdir(CLEANUP => 1);
21 my $home = "$tmpdir/pi-home";
22 my $err = "$tmpdir/stderr.log";
23 my $out = "$tmpdir/stdout.log";
24 my $pi_home = "$home/.public-inbox";
25 my $pi_config = "$pi_home/config";
26 my $maindir = "$tmpdir/main.git";
27 my $main_bin = getcwd()."/t/main-bin";
28 my $main_path = "$main_bin:$ENV{PATH}"; # for spamc ham mock
29 my $group = 'test-nntpd';
30 my $addr = $group . '@example.com';
31 my $cfgpfx = "publicinbox.$group";
32 my $failbox = "$home/fail.mbox";
33 local $ENV{PI_EMERGENCY} = $failbox;
34 my $mda = 'blib/script/public-inbox-mda';
35 my $nntpd = 'blib/script/public-inbox-nntpd';
36 my $init = 'blib/script/public-inbox-init';
37 my $index = 'blib/script/public-inbox-index';
38
39 my %opts = (
40         LocalAddr => '127.0.0.1',
41         ReuseAddr => 1,
42         Proto => 'tcp',
43         Type => SOCK_STREAM,
44         Listen => 1024,
45 );
46 my $sock = IO::Socket::INET->new(%opts);
47 my $pid;
48 END { kill 'TERM', $pid if defined $pid };
49 {
50         local $ENV{HOME} = $home;
51         system($init, $group, $maindir, 'http://example.com/', $addr);
52
53         # ensure successful message delivery
54         {
55                 local $ENV{ORIGINAL_RECIPIENT} = $addr;
56                 my $simple = Email::Simple->new(<<EOF);
57 From: Me <me\@example.com>
58 To: You <you\@example.com>
59 Cc: $addr
60 Message-Id: <nntp\@example.com>
61 Subject: hihi
62 Date: Thu, 01 Jan 1970 06:06:06 +0000
63
64 nntp
65 EOF
66                 my $in = $simple->as_string;
67                 local $ENV{PATH} = $main_path;
68                 IPC::Run::run([$mda], \$in);
69                 is(0, $?, 'ran MDA correctly');
70                 is(0, system($index, $maindir), 'indexed git dir');
71         }
72
73         ok($sock, 'sock created');
74         $! = 0;
75         my $fl = fcntl($sock, F_GETFD, 0);
76         ok(! $!, 'no error from fcntl(F_GETFD)');
77         is($fl, FD_CLOEXEC, 'cloexec set by default (Perl behavior)');
78         $pid = fork;
79         if ($pid == 0) {
80                 use POSIX qw(dup2);
81                 # pretend to be systemd
82                 fcntl($sock, F_SETFD, $fl &= ~FD_CLOEXEC);
83                 dup2(fileno($sock), 3) or die "dup2 failed: $!\n";
84                 $ENV{LISTEN_PID} = $$;
85                 $ENV{LISTEN_FDS} = 1;
86                 exec $nntpd, "--stdout=$out", "--stderr=$err";
87                 die "FAIL: $!\n";
88         }
89         ok(defined $pid, 'forked nntpd process successfully');
90         $! = 0;
91         fcntl($sock, F_SETFD, $fl |= FD_CLOEXEC);
92         ok(! $!, 'no error from fcntl(F_SETFD)');
93         my $host_port = $sock->sockhost . ':' . $sock->sockport;
94         my $n = Net::NNTP->new($host_port);
95         my $list = $n->list;
96         is_deeply($list, { $group => [ qw(1 1 n) ] }, 'LIST works');
97         is_deeply([$n->group($group)], [ qw(0 1 1), $group ], 'GROUP works');
98
99         %opts = (
100                 PeerAddr => $host_port,
101                 Proto => 'tcp',
102                 Type => SOCK_STREAM,
103                 Timeout => 1,
104         );
105         my $mid = '<nntp@example.com>';
106         my %xhdr = (
107                 'message-id' => $mid,
108                 'subject' => 'hihi',
109                 'date' => 'Thu, 01 Jan 1970 06:06:06 +0000',
110                 'from' => 'Me <me@example.com>',
111                 'to' => 'You <you@example.com>',
112                 'cc' => $addr,
113                 'xref' => "example.com $group:1"
114         );
115
116         my $s = IO::Socket::INET->new(%opts);
117         sysread($s, my $buf, 4096);
118         is($buf, "201 server ready - post via email\r\n", 'got greeting');
119         $s->autoflush(1);
120
121         while (my ($k, $v) = each %xhdr) {
122                 is_deeply($n->xhdr("$k $mid"), { $mid => $v },
123                           "XHDR $k by message-id works");
124                 is_deeply($n->xhdr("$k 1"), { 1 => $v },
125                           "$k by article number works");
126                 is_deeply($n->xhdr("$k 1-"), { 1 => $v },
127                           "$k by article range works");
128                 next;
129                 $buf = '';
130                 syswrite($s, "HDR $k $mid\r\n");
131                 do {
132                         sysread($s, $buf, 4096, length($buf));
133                 } until ($buf =~ /^[^2]../ || $buf =~ /\r\n\.\r\n\z/);
134                 my @r = split("\r\n", $buf);
135                 like($r[0], qr/\A224 /, '224 response for HDR');
136                 is($r[1], "0 $v", 'got expected response for HDR');
137         }
138
139         {
140                 my $nogroup = Net::NNTP->new($host_port);
141                 while (my ($k, $v) = each %xhdr) {
142                         is_deeply($nogroup->xhdr("$k $mid"), { $mid => $v },
143                                   "$k by message-id works without group");
144                 }
145         }
146
147         is_deeply($n->xover('1-'), {
148                 '1' => ['hihi',
149                         'Me <me@example.com>',
150                         'Thu, 01 Jan 1970 06:06:06 +0000',
151                         '<nntp@example.com>',
152                         '',
153                         '202',
154                         '1' ] }, "XOVER range works");
155
156         is_deeply($n->xover('1'), {
157                 '1' => ['hihi',
158                         'Me <me@example.com>',
159                         'Thu, 01 Jan 1970 06:06:06 +0000',
160                         '<nntp@example.com>',
161                         '',
162                         '202',
163                         '1' ] }, "XOVER by article works");
164
165         {
166                 syswrite($s, "OVER $mid\r\n");
167                 $buf = '';
168                 do {
169                         sysread($s, $buf, 4096, length($buf));
170                 } until ($buf =~ /^[^2]../ || $buf =~ /\r\n\.\r\n\z/);
171                 my @r = split("\r\n", $buf);
172                 like($r[0], qr/^224 /, 'got 224 response for OVER');
173                 is($r[1], "0\thihi\tMe <me\@example.com>\t" .
174                         "Thu, 01 Jan 1970 06:06:06 +0000\t" .
175                         "$mid\t\t202\t1", 'OVER by Message-ID works');
176                 is($r[2], '.', 'correctly terminated response');
177         }
178
179         ok(kill('TERM', $pid), 'killed nntpd');
180         $pid = undef;
181         waitpid(-1, 0);
182 }
183
184 done_testing();
185
186 1;