PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/t/lib/Net/IMAP/Server/Test.pm

https://github.com/bestpractical/net-imap-server
Perl | 229 lines | 196 code | 28 blank | 5 comment | 19 complexity | 271ec002f47e0523e820151e29068efd MD5 | raw file
  1. package Net::IMAP::Server::Test;
  2. use base qw/Test::More/;
  3. use strict;
  4. use warnings;
  5. use Socket;
  6. use AnyEvent;
  7. AnyEvent::detect();
  8. use IO::Socket::SSL;
  9. use Time::HiRes qw();
  10. my $PPID = $$;
  11. sub PORT() { 2000 + $PPID*2 }
  12. sub SSL_PORT() { 2001 + $PPID*2 }
  13. sub import_extra {
  14. my $class = shift;
  15. Test::More->export_to_level(2);
  16. binmode $class->builder->output, ":utf8";
  17. }
  18. my $pid;
  19. sub start_server {
  20. my $class = shift;
  21. $class->stop_server;
  22. unless ( $pid = fork ) {
  23. require Net::IMAP::Server::Test::Server;
  24. Net::IMAP::Server::Test::Server->new(
  25. auth_class => "Net::IMAP::Server::Test::Auth",
  26. port => "127.0.0.1:".PORT,
  27. ssl_port => "127.0.0.1:".SSL_PORT,
  28. group => $(,
  29. user => $<,
  30. @_
  31. )->run;
  32. exit;
  33. }
  34. return $pid;
  35. }
  36. sub start_server_ok {
  37. my $class = shift;
  38. my $msg = @_ % 2 ? shift @_ : "Server started";
  39. local $Test::Builder::Level = $Test::Builder::Level + 1;
  40. Test::More::ok($class->start_server(@_), $msg);
  41. }
  42. sub as {
  43. my $class = shift;
  44. my ($as) = @_;
  45. $as =~ s/\W//g;
  46. $as = "SOCKET_$as";
  47. my $newclass = $class."::".$as;
  48. return $newclass if exists $class->builder->{$as};
  49. eval "{ package $newclass; our \@ISA = 'Net::IMAP::Server::Test'; sub socket_key { '$as' }; }";
  50. $class->builder->{$as} = undef;
  51. return $newclass;
  52. }
  53. sub socket_key { "SOCKET" };
  54. sub connect {
  55. my $class = shift;
  56. my %args = (
  57. PeerAddr => '127.0.0.1',
  58. PeerPort => SSL_PORT,
  59. Class => "IO::Socket::SSL",
  60. SSL_ca_file => "certs/server-cert.pem",
  61. @_
  62. );
  63. my $socketclass = delete $args{Class};
  64. my $start = Time::HiRes::time();
  65. while (Time::HiRes::time() - $start < 10) {
  66. my $socket = $socketclass->new( %args );
  67. return $class->builder->{$class->socket_key} = $socket if $socket;
  68. Time::HiRes::sleep(0.1);
  69. }
  70. return;
  71. }
  72. sub connected {
  73. my $class = shift;
  74. my $socket = $class->get_socket;
  75. return 0 unless $socket->connected;
  76. my $buf;
  77. # We intentionally use the non-OO recv function here,
  78. # IO::Socket::SSL doesn't define a recv, and we want the low-level,
  79. # not under a layer version, anyways.
  80. my $waiting = recv($socket, $buf, 1, MSG_PEEK | MSG_DONTWAIT);
  81. # Undef if there's nothing currently waiting
  82. return 1 if not defined $waiting;
  83. # True if there is, false if the connection is closed
  84. return $waiting;
  85. }
  86. sub get_socket {
  87. my $class = shift;
  88. return $class->builder->{$class->socket_key};
  89. }
  90. sub disconnect {
  91. my $class = shift;
  92. $class->get_socket->close;
  93. $class->builder->{$class->socket_key} = undef;
  94. }
  95. sub connect_ok {
  96. local $Test::Builder::Level = $Test::Builder::Level + 1;
  97. my $class = shift;
  98. my $msg = @_ % 2 ? shift @_ : "Connected successfully";
  99. my $socket = $class->connect(@_);
  100. Test::More::ok($socket, $msg);
  101. Test::More::like($socket->getline, qr/^\* OK\b/, "Got connection message");
  102. }
  103. sub start_tls {
  104. my $class = shift;
  105. IO::Socket::SSL->start_SSL(
  106. $class->get_socket,
  107. SSL_ca_file => "certs/server-cert.pem",
  108. );
  109. }
  110. sub start_tls_ok {
  111. local $Test::Builder::Level = $Test::Builder::Level + 1;
  112. my $class = shift;
  113. my ($msg) = @_;
  114. my $socket = $class->get_socket || return Test::More::fail("Not connected!");
  115. $class->start_tls($socket);
  116. Test::More::diag(IO::Socket::SSL::errstr())
  117. unless $socket->isa("IO::Socket::SSL");
  118. Test::More::ok(
  119. $socket->isa("IO::Socket::SSL"),
  120. $msg || "Negotiated TLS",
  121. );
  122. }
  123. sub send_cmd {
  124. local $Test::Builder::Level = $Test::Builder::Level + 1;
  125. my $class = shift;
  126. my $cmd = shift;
  127. $class->send_line("tag $cmd", @_);
  128. }
  129. sub send_line {
  130. local $Test::Builder::Level = $Test::Builder::Level + 1;
  131. my $class = shift;
  132. my ($cmd, $socket) = (@_, $class->get_socket);
  133. my $response = "";
  134. local $SIG{ALRM} = sub { die "Timeout" };
  135. alarm(5);
  136. eval {
  137. $socket->print("$cmd\r\n");
  138. while (my $line = $socket->getline) {
  139. $response .= $line;
  140. last if $line =~ /^(?:\+\s*$|tag\b)/;
  141. }
  142. };
  143. Test::More::fail("$cmd: Timed out waiting for response")
  144. if ($@ || "") =~ /Timeout/;
  145. alarm(0);
  146. return $response;
  147. }
  148. sub cmd_ok {
  149. local $Test::Builder::Level = $Test::Builder::Level + 1;
  150. my $class = shift;
  151. my ($cmd, $msg) = @_;
  152. my $socket = $class->get_socket || return Test::More::fail("Not connected: $cmd");
  153. my $response = $class->send_cmd($cmd, $socket);
  154. Test::More::like($response, qr/^tag OK\b/m, $msg || "$cmd");
  155. return $response;
  156. }
  157. sub cmd_like {
  158. local $Test::Builder::Level = $Test::Builder::Level + 1;
  159. my $class = shift;
  160. $class->_send_like("send_cmd", @_);
  161. }
  162. sub line_like {
  163. local $Test::Builder::Level = $Test::Builder::Level + 1;
  164. my $class = shift;
  165. $class->_send_like("send_line", @_);
  166. }
  167. sub _send_like {
  168. local $Test::Builder::Level = $Test::Builder::Level + 1;
  169. my $class = shift;
  170. my ($method, $cmd, @match) = @_;
  171. my $socket = $class->get_socket || return Test::More::fail("Not connected: $cmd");
  172. my $response = $class->$method($cmd, $socket);
  173. my @got = split /\r\n/, $response;
  174. Test::More::fail("Got wrong number of lines of response (expect @{[scalar @match]}, got @{[scalar @got]})")
  175. unless @match == @got;
  176. for my $i (0..$#match) {
  177. my $match = ref $match[$i] ? $match[$i] : qr/^\Q$match[$i]\E\s*(?:\b|$)/;
  178. Test::More::like($got[$i], $match, "Line @{[$i+1]} of $cmd response matched");
  179. }
  180. return wantarray ? @got : $response;
  181. }
  182. sub mailbox_list {
  183. local $Test::Builder::Level = $Test::Builder::Level + 1;
  184. my $class = shift;
  185. my ($base, $pattern) = @_;
  186. $base ||= "";
  187. $pattern ||= "*";
  188. my $ret = $class->send_cmd(qq{LIST "$base" "$pattern"});
  189. my %mailboxes;
  190. $mailboxes{$2} = $1 while $ret =~ m{^\* LIST \((\\\S+(?:\s+\\\S+)*)\) "/" "(.*?)"}mg;
  191. return %mailboxes;
  192. }
  193. sub stop_server {
  194. return unless $pid;
  195. local $?;
  196. kill 2, $pid;
  197. 1 while wait > 0;
  198. }
  199. $SIG{$_} = sub {exit} for qw/TERM INT QUIT/;
  200. END { stop_server() }
  201. 1;