File Coverage

blib/lib/JMAP/Tester/WebSocket.pm
Criterion Covered Total %
statement 35 94 37.2
branch 0 20 0.0
condition 0 14 0.0
subroutine 12 18 66.6
pod 1 2 50.0
total 48 148 32.4


line stmt bran cond sub pod time code
1 1     1   637 use v5.10.0;
  1         3  
2 1     1   6 use warnings;
  1         1  
  1         46  
3              
4             package JMAP::Tester::WebSocket;
5             # ABSTRACT: a WebSocket JMAP client made for testing JMAP servers
6             $JMAP::Tester::WebSocket::VERSION = '0.001';
7 1     1   561 use Moo;
  1         10949  
  1         5  
8 1     1   2169 use IO::Async::Loop;
  1         42648  
  1         40  
9 1     1   501 use Net::Async::WebSocket::Client 0.13;
  1         71061  
  1         32  
10 1     1   9 use Protocol::WebSocket::Request;
  1         3  
  1         24  
11 1     1   559 use Params::Util qw(_HASH0 _ARRAY0);
  1         2509  
  1         71  
12 1     1   578 use Data::Dumper;
  1         6003  
  1         69  
13 1     1   8 use Scalar::Util qw(weaken);
  1         3  
  1         43  
14 1     1   482 use Try::Tiny;
  1         1217  
  1         58  
15              
16 1     1   787 use JMAP::Tester::WebSocket::Response;
  1         4  
  1         34  
17 1     1   450 use JMAP::Tester::WebSocket::Result::Failure;
  1         2  
  1         928  
18              
19             extends qw(JMAP::Tester);
20              
21             has +json_codec => (
22             is => 'bare',
23             handles => {
24             json_encode => 'encode',
25             json_decode => 'decode',
26             },
27             default => sub {
28             require JSON;
29              
30             # Not ->utf8-> or we die decoding things with "wide character"...
31             # Maybe to be fixed in Protocol::WebSocket? Or IO::Async is doing this
32             # for us?
33             return JSON->new->convert_blessed;
34             },
35             );
36              
37              
38             has 'ws_api_uri' => (
39             is => 'rw',
40             required => 1,
41             );
42              
43             has cache_connection => (
44             is => 'ro',
45             default => 0,
46             );
47              
48             has 'authorization' => (
49             is => 'rw',
50             predicate => 'has_authorization',
51             );
52              
53             has _cached_client => (
54             is => 'rw',
55             );
56              
57             has loop => (
58             is => 'rw',
59             default => sub { IO::Async::Loop->new },
60             );
61              
62             sub request {
63 0     0 1   my ($self, $input_request) = @_;
64              
65 0           state $ident = 'a';
66 0           my %seen;
67             my @suffixed;
68              
69 0           my %default_args = %{ $self->default_arguments };
  0            
70              
71 0 0         my $request = _ARRAY0($input_request)
72             ? { methodCalls => $input_request }
73             : { %$input_request };
74              
75 0           for my $call (@{ $request->{methodCalls} }) {
  0            
76 0           my $copy = [ @$call ];
77 0 0         if (defined $copy->[2]) {
78 0           $seen{$call->[2]}++;
79             } else {
80 0           my $next;
81 0           do { $next = $ident++ } until ! $seen{$ident}++;
  0            
82 0           $copy->[2] = $next;
83             }
84              
85             my %arg = (
86             %default_args,
87 0   0       %{ $copy->[1] // {} },
  0            
88             );
89              
90 0           for my $key (keys %arg) {
91 0 0 0       if ( ref $arg{$key}
      0        
92             && ref $arg{$key} eq 'SCALAR'
93 0           && ! defined ${ $arg{$key} }
94             ) {
95 0           delete $arg{$key};
96             }
97             }
98              
99 0           $copy->[1] = \%arg;
100              
101 0           push @suffixed, $copy;
102             }
103              
104 0           $request->{methodCalls} = \@suffixed;
105              
106             $request = $request->{methodCalls}
107 0 0 0       if $ENV{JMAP_TESTER_NO_WRAPPER} && _ARRAY0($input_request);
108              
109 0           my $json = $self->json_encode($request);
110              
111 0   0       my $client = $self->_cached_client || $self->connect_ws;
112              
113 0           $client->send_text_frame($json);
114              
115 0           my $res = $self->loop->run;
116              
117 0 0         unless ($self->_cached_client) {
118 0           $self->loop->remove($client);
119             }
120              
121 0           return $self->_jresponse_from_wsresponse($res);
122             }
123              
124             sub connect_ws {
125 0     0 0   my ($self) = @_;
126              
127 0           my $loop = $self->loop;
128              
129 0           weaken($loop);
130              
131             my $client = Net::Async::WebSocket::Client->new(
132             on_text_frame => sub {
133 0     0     my ($c, $f) = @_;
134              
135 0           $loop->stop($f);
136             },
137 0           );
138              
139 0           $client->{framebuffer} = Protocol::WebSocket::Frame->new(
140             max_payload_size => 0
141             );
142              
143 0           $self->loop->add($client);
144              
145 0 0         $client->connect(
146             url => $self->ws_api_uri,
147             req => Protocol::WebSocket::Request->new(
148             headers => [
149             ( $self->authorization
150             ? ( Authorization => $self->authorization )
151             : ()
152             ),
153             ],
154             subprotocol => 'jmap',
155             ),
156             )->get;
157              
158 0 0         if ($self->cache_connection) {
159 0           $self->_cached_client($client);
160             }
161              
162 0           return $client;
163             }
164              
165             sub _jresponse_from_wsresponse {
166 0     0     my ($self, $ws_res) = @_;
167              
168 0           my ($data, $error);
169              
170             try {
171 0     0     $data = $self->apply_json_types($self->json_decode( $ws_res ));
172             } catch {
173 0     0     $error = $_;
174 0           };
175              
176 0 0         if (defined $error) {
177 0           return JMAP::Tester::WebSocket::Result::Failure->new(
178             ws_response => $ws_res,
179             ident => $error,
180             );
181             }
182              
183 0           my ($items, $props);
184 0 0         if (_HASH0($data)) {
    0          
185 0           $props = $data;
186 0           $items = $props->{methodResponses};
187             } elsif (_ARRAY0($data)) {
188 0           $props = {};
189 0           $items = $data;
190             } else {
191 0           abort("illegal response to JMAP request: $data");
192             }
193              
194 0           return JMAP::Tester::WebSocket::Response->new({
195             items => $items,
196             ws_response => $ws_res,
197             wrapper_properties => $props,
198             });
199             }
200              
201             1;
202              
203             =pod
204              
205             =encoding UTF-8
206              
207             =head1 NAME
208              
209             JMAP::Tester::WebSocket - a WebSocket JMAP client made for testing JMAP servers
210              
211             =head1 VERSION
212              
213             version 0.001
214              
215             =head1 SYNOPSIS
216              
217             use JMAP::Tester::WebSocket;
218              
219             my $jtest = JMAP::Tester::WebSocket->new({
220             ws_uri => 'ws://jmap.local/account/123',
221             });
222              
223             my $response = $jtest->request([
224             [ getMailboxes => {} ],
225             [ getMessageUpdates => { sinceState => "123" } ],
226             ]);
227              
228             =head1 DESCRIPTION
229              
230             This module provides a WebSockets wrapper around L.
231              
232             See L for more information.
233              
234             =head1 SEE ALSO
235              
236             L - a JMAP client made for testing JMAP servers
237              
238             =head1 AUTHOR
239              
240             Matthew Horsfall
241              
242             =head1 COPYRIGHT AND LICENSE
243              
244             This software is copyright (c) 2018 by FastMail, Ltd.
245              
246             This is free software; you can redistribute it and/or modify it under
247             the same terms as the Perl 5 programming language system itself.
248              
249             =cut
250              
251             __END__