The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl -T
use 5.006;
use strict;
use warnings FATAL => 'all';
use Test::More;

use Protocol::WebSocket::Handshake::Client;
use Protocol::WebSocket::Frame;
use IO::Socket::INET;

my $child;
sub cleanup { kill 9 => $child if $child && $child > 0 }
$SIG{ALRM} = sub { cleanup; die "test timed out\n"; };
alarm(10);

my $listen = IO::Socket::INET->new(Listen => 2, Proto => 'tcp', Timeout => 5) or die "$!";

unless ($child = fork) {
  delete $SIG{ALRM};
  require Net::WebSocket::Server;

  Net::WebSocket::Server->new(
    listen => $listen,
    on_connect => sub {
      my ($serv, $conn) = @_;
      $conn->on(
        handshake => sub {
          my ($conn, $hs) = @_;
          die "bad handshake origin: " . $hs->req->origin unless $hs->req->origin eq 'http://localhost';
          die "bad subprotocol: " . $hs->req->subprotocol unless $hs->req->subprotocol eq 'test subprotocol';
        },
        ready => sub {
          my ($conn) = @_;
          $conn->send_binary("ready");
        },
        utf8 => sub {
          my ($conn, $msg) = @_;
          $conn->send_utf8("utf8(".length($msg).") = $msg");
        },
        binary => sub {
          my ($conn, $msg) = @_;
          $conn->send_binary("binary(".length($msg).") = $msg");
        },
        pong => sub {
          my ($conn, $msg) = @_;
          $conn->send_binary("pong(".length($msg).") = $msg");
        },
        disconnect => sub {
          my ($conn, $code, $reason) = @_;
          die "bad disconnect code" unless defined $code && $code == 4242;
          die "bad disconnect reason" unless defined $reason && $reason eq 'test server shutdown cleanly';
          $serv->shutdown();
        },
      );
    },
  )->start;

  exit;
}

my ($port, $sock);

subtest "initialize client socket" => sub {
  $port = $listen->sockport;
  $sock = IO::Socket::INET->new(PeerPort => $port, Proto => 'tcp', PeerAddr => 'localhost')
       || IO::Socket::INET->new(PeerPort => $port, Proto => 'tcp', PeerAddr => '127.0.0.1')
       or die "$! (maybe your system does not have a localhost at all, 'localhost' or 127.0.0.1)";
  ok(1);
};

my $buf = "";
my $hs;

subtest "handshake send" => sub {
  $hs = Protocol::WebSocket::Handshake::Client->new(url => 'ws://localhost/testserver');
  $hs->req->subprotocol("test subprotocol");
  print $sock $hs->to_string;
  ok(1);
};

subtest "handshake recv" => sub {
  while(sysread($sock, $buf, 8192, length($buf))) {
    $hs->parse($buf);
    last if $hs->is_done;
  }

  ok(!$hs->error, "completed handshake with server without error");
};

my $parser;

subtest "initialize parser" => sub {
  $parser = new Protocol::WebSocket::Frame();
  $parser->append($buf);
  ok(1);
};

subtest "ready message" => sub {
  my $bytes = _recv($sock => $parser);
  ok($parser->is_binary, "expected binary message");
  is($bytes, "ready", "expected welcome 'ready' message");
};

subtest "echo utf8" => sub {
  foreach my $msg ("simple", "", ("a" x 32768), "unicode \u2603 snowman", "hiragana \u3072\u3089\u304c\u306a null \x00 ctrls \cA \cF \n \e del \x7f end") {
    print $sock Protocol::WebSocket::Frame->new(type=>'text', buffer=>$msg)->to_bytes;
    my $bytes = _recv($sock => $parser);
    ok($parser->is_text, "expected text message");
    is($bytes, "utf8(" . length($msg) . ") = $msg");
  }
};

subtest "echo binary" => sub {
  foreach my $msg ("simple", "", ("a" x 32768), "unicode \u2603 snowman", "hiragana \u3072\u3089\u304c\u306a null \x00 ctrls \cA \cF \n \e del \x7f end", join("", map{chr($_)} 0..255)) {
    print $sock Protocol::WebSocket::Frame->new(type=>'binary', buffer=>$msg)->to_bytes;
    my $bytes = _recv($sock => $parser);
    ok($parser->is_binary, "expected binary message");
    is($bytes, "binary(" . length($msg) . ") = $msg");
  }
};

subtest "echo pong" => sub {
  foreach my $msg ("simple", "", ("a" x 32768), "unicode \u2603 snowman", "hiragana \u3072\u3089\u304c\u306a null \x00 ctrls \cA \cF \n \e del \x7f end", join("", map{chr($_)} 0..255)) {
    print $sock Protocol::WebSocket::Frame->new(type=>'pong', buffer=>$msg)->to_bytes;
    my $bytes = _recv($sock => $parser);
    ok($parser->is_binary, "expected binary message");
    is($bytes, "pong(" . length($msg) . ") = $msg");
  }
};

subtest "server shutdown" => sub {
  ok((kill 0 => $child), "child should still be alive");
  print $sock Protocol::WebSocket::Frame->new(type=>'close', buffer=>pack("n",4242)."test server shutdown cleanly")->to_bytes;
  waitpid $child, 0;
  ok(!(kill 0 => $child), "child should have shut down cleanly");
};

done_testing();
cleanup();

sub _recv {
  my ($sock, $parser) = @_;

  my $message;
  while (!defined($message = $parser->next_bytes)) {
    my $data;
    die "expected read but socket seems to be disconnected" unless defined sysread($sock, $data, 8192);
    $parser->append($data);
  }
  return $message;
}