Home | History | Annotate | Download | only in test
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2012, Google Inc.
      4 # All rights reserved.
      5 #
      6 # Redistribution and use in source and binary forms, with or without
      7 # modification, are permitted provided that the following conditions are
      8 # met:
      9 #
     10 #     * Redistributions of source code must retain the above copyright
     11 # notice, this list of conditions and the following disclaimer.
     12 #     * Redistributions in binary form must reproduce the above
     13 # copyright notice, this list of conditions and the following disclaimer
     14 # in the documentation and/or other materials provided with the
     15 # distribution.
     16 #     * Neither the name of Google Inc. nor the names of its
     17 # contributors may be used to endorse or promote products derived from
     18 # this software without specific prior written permission.
     19 #
     20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31 
     32 
     33 """Test for end-to-end."""
     34 
     35 
     36 import logging
     37 import os
     38 import signal
     39 import socket
     40 import subprocess
     41 import sys
     42 import time
     43 import unittest
     44 
     45 import set_sys_path  # Update sys.path to locate mod_pywebsocket module.
     46 
     47 from test import client_for_testing
     48 from test import mux_client_for_testing
     49 
     50 
     51 # Special message that tells the echo server to start closing handshake
     52 _GOODBYE_MESSAGE = 'Goodbye'
     53 
     54 # If you want to use external server to run end to end tests, set following
     55 # parameters correctly.
     56 _use_external_server = False
     57 _external_server_port = 0
     58 
     59 
     60 # Test body functions
     61 def _echo_check_procedure(client):
     62     client.connect()
     63 
     64     client.send_message('test')
     65     client.assert_receive('test')
     66     client.send_message('helloworld')
     67     client.assert_receive('helloworld')
     68 
     69     client.send_close()
     70     client.assert_receive_close()
     71 
     72     client.assert_connection_closed()
     73 
     74 
     75 def _echo_check_procedure_with_binary(client):
     76     client.connect()
     77 
     78     client.send_message('binary', binary=True)
     79     client.assert_receive('binary', binary=True)
     80     client.send_message('\x00\x80\xfe\xff\x00\x80', binary=True)
     81     client.assert_receive('\x00\x80\xfe\xff\x00\x80', binary=True)
     82 
     83     client.send_close()
     84     client.assert_receive_close()
     85 
     86     client.assert_connection_closed()
     87 
     88 
     89 def _echo_check_procedure_with_goodbye(client):
     90     client.connect()
     91 
     92     client.send_message('test')
     93     client.assert_receive('test')
     94 
     95     client.send_message(_GOODBYE_MESSAGE)
     96     client.assert_receive(_GOODBYE_MESSAGE)
     97 
     98     client.assert_receive_close()
     99     client.send_close()
    100 
    101     client.assert_connection_closed()
    102 
    103 
    104 def _echo_check_procedure_with_code_and_reason(client, code, reason):
    105     client.connect()
    106 
    107     client.send_close(code, reason)
    108     client.assert_receive_close(code, reason)
    109 
    110     client.assert_connection_closed()
    111 
    112 
    113 def _unmasked_frame_check_procedure(client):
    114     client.connect()
    115 
    116     client.send_message('test', mask=False)
    117     client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '')
    118 
    119     client.assert_connection_closed()
    120 
    121 
    122 def _mux_echo_check_procedure(mux_client):
    123     mux_client.connect()
    124     mux_client.send_flow_control(1, 1024)
    125 
    126     logical_channel_options = client_for_testing.ClientOptions()
    127     logical_channel_options.server_host = 'localhost'
    128     logical_channel_options.server_port = 80
    129     logical_channel_options.origin = 'http://localhost'
    130     logical_channel_options.resource = '/echo'
    131     mux_client.add_channel(2, logical_channel_options)
    132     mux_client.send_flow_control(2, 1024)
    133 
    134     mux_client.send_message(2, 'test')
    135     mux_client.assert_receive(2, 'test')
    136 
    137     mux_client.add_channel(3, logical_channel_options)
    138     mux_client.send_flow_control(3, 1024)
    139 
    140     mux_client.send_message(2, 'hello')
    141     mux_client.send_message(3, 'world')
    142     mux_client.assert_receive(2, 'hello')
    143     mux_client.assert_receive(3, 'world')
    144 
    145     # Don't send close message on channel id 1 so that server-initiated
    146     # closing handshake won't occur.
    147     mux_client.send_close(2)
    148     mux_client.send_close(3)
    149     mux_client.assert_receive_close(2)
    150     mux_client.assert_receive_close(3)
    151 
    152     mux_client.send_physical_connection_close()
    153     mux_client.assert_physical_connection_receive_close()
    154 
    155 
    156 class EndToEndTest(unittest.TestCase):
    157     """An end-to-end test that launches pywebsocket standalone server as a
    158     separate process, connects to it using the client_for_testing module, and
    159     checks if the server behaves correctly by exchanging opening handshake and
    160     frames over a TCP connection.
    161     """
    162 
    163     def setUp(self):
    164         self.server_stderr = None
    165         self.top_dir = os.path.join(os.path.split(__file__)[0], '..')
    166         os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
    167         self.standalone_command = os.path.join(
    168             self.top_dir, 'mod_pywebsocket', 'standalone.py')
    169         self.document_root = os.path.join(self.top_dir, 'example')
    170         s = socket.socket()
    171         s.bind(('localhost', 0))
    172         (_, self.test_port) = s.getsockname()
    173         s.close()
    174 
    175         self._options = client_for_testing.ClientOptions()
    176         self._options.server_host = 'localhost'
    177         self._options.origin = 'http://localhost'
    178         self._options.resource = '/echo'
    179 
    180         # TODO(toyoshim): Eliminate launching a standalone server on using
    181         # external server.
    182 
    183         if _use_external_server:
    184             self._options.server_port = _external_server_port
    185         else:
    186             self._options.server_port = self.test_port
    187 
    188     def _run_python_command(self, commandline, stdout=None, stderr=None):
    189         return subprocess.Popen([sys.executable] + commandline, close_fds=True,
    190                                 stdout=stdout, stderr=stderr)
    191 
    192     def _run_server(self, allow_draft75=False):
    193         args = [self.standalone_command,
    194                 '-H', 'localhost',
    195                 '-V', 'localhost',
    196                 '-p', str(self.test_port),
    197                 '-P', str(self.test_port),
    198                 '-d', self.document_root]
    199 
    200         # Inherit the level set to the root logger by test runner.
    201         root_logger = logging.getLogger()
    202         log_level = root_logger.getEffectiveLevel()
    203         if log_level != logging.NOTSET:
    204             args.append('--log-level')
    205             args.append(logging.getLevelName(log_level).lower())
    206 
    207         if allow_draft75:
    208             args.append('--allow-draft75')
    209 
    210         return self._run_python_command(args,
    211                                         stderr=self.server_stderr)
    212 
    213     def _kill_process(self, pid):
    214         if sys.platform in ('win32', 'cygwin'):
    215             subprocess.call(
    216                 ('taskkill.exe', '/f', '/pid', str(pid)), close_fds=True)
    217         else:
    218             os.kill(pid, signal.SIGKILL)
    219 
    220     def _run_hybi_test_with_client_options(self, test_function, options):
    221         server = self._run_server()
    222         try:
    223             # TODO(tyoshino): add some logic to poll the server until it
    224             # becomes ready
    225             time.sleep(0.2)
    226 
    227             client = client_for_testing.create_client(options)
    228             try:
    229                 test_function(client)
    230             finally:
    231                 client.close_socket()
    232         finally:
    233             self._kill_process(server.pid)
    234 
    235     def _run_hybi_test(self, test_function):
    236         self._run_hybi_test_with_client_options(test_function, self._options)
    237 
    238     def _run_hybi_deflate_test(self, test_function):
    239         server = self._run_server()
    240         try:
    241             time.sleep(0.2)
    242 
    243             self._options.enable_deflate_stream()
    244             client = client_for_testing.create_client(self._options)
    245             try:
    246                 test_function(client)
    247             finally:
    248                 client.close_socket()
    249         finally:
    250             self._kill_process(server.pid)
    251 
    252     def _run_hybi_deflate_frame_test(self, test_function):
    253         server = self._run_server()
    254         try:
    255             time.sleep(0.2)
    256 
    257             self._options.enable_deflate_frame()
    258             client = client_for_testing.create_client(self._options)
    259             try:
    260                 test_function(client)
    261             finally:
    262                 client.close_socket()
    263         finally:
    264             self._kill_process(server.pid)
    265 
    266     def _run_hybi_close_with_code_and_reason_test(self, test_function, code,
    267                                                   reason):
    268         server = self._run_server()
    269         try:
    270             time.sleep(0.2)
    271 
    272             client = client_for_testing.create_client(self._options)
    273             try:
    274                 test_function(client, code, reason)
    275             finally:
    276                 client.close_socket()
    277         finally:
    278             self._kill_process(server.pid)
    279 
    280     def _run_hybi_http_fallback_test(self, options, status):
    281         server = self._run_server()
    282         try:
    283             time.sleep(0.2)
    284 
    285             client = client_for_testing.create_client(options)
    286             try:
    287                 client.connect()
    288                 self.fail('Could not catch HttpStatusException')
    289             except client_for_testing.HttpStatusException, e:
    290                 self.assertEqual(status, e.status)
    291             except Exception, e:
    292                 self.fail('Catch unexpected exception')
    293             finally:
    294                 client.close_socket()
    295         finally:
    296             self._kill_process(server.pid)
    297 
    298     def _run_hybi_mux_test(self, test_function):
    299         server = self._run_server()
    300         try:
    301             time.sleep(0.2)
    302 
    303             client = mux_client_for_testing.MuxClient(self._options)
    304             try:
    305                 test_function(client)
    306             finally:
    307                 client.close_socket()
    308         finally:
    309             self._kill_process(server.pid)
    310 
    311     def test_echo(self):
    312         self._run_hybi_test(_echo_check_procedure)
    313 
    314     def test_echo_binary(self):
    315         self._run_hybi_test(_echo_check_procedure_with_binary)
    316 
    317     def test_echo_server_close(self):
    318         self._run_hybi_test(_echo_check_procedure_with_goodbye)
    319 
    320     def test_unmasked_frame(self):
    321         self._run_hybi_test(_unmasked_frame_check_procedure)
    322 
    323     def test_echo_deflate(self):
    324         self._run_hybi_deflate_test(_echo_check_procedure)
    325 
    326     def test_echo_deflate_server_close(self):
    327         self._run_hybi_deflate_test(_echo_check_procedure_with_goodbye)
    328 
    329     def test_echo_deflate_frame(self):
    330         self._run_hybi_deflate_frame_test(_echo_check_procedure)
    331 
    332     def test_echo_deflate_frame_server_close(self):
    333         self._run_hybi_deflate_frame_test(
    334             _echo_check_procedure_with_goodbye)
    335 
    336     def test_echo_close_with_code_and_reason(self):
    337         self._options.resource = '/close'
    338         self._run_hybi_close_with_code_and_reason_test(
    339             _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun')
    340 
    341     def test_echo_close_with_empty_body(self):
    342         self._options.resource = '/close'
    343         self._run_hybi_close_with_code_and_reason_test(
    344             _echo_check_procedure_with_code_and_reason, None, '')
    345 
    346     def test_mux_echo(self):
    347         self._run_hybi_mux_test(_mux_echo_check_procedure)
    348 
    349     def test_close_on_protocol_error(self):
    350         """Tests that the server sends a close frame with protocol error status
    351         code when the client sends data with some protocol error.
    352         """
    353 
    354         def test_function(client):
    355             client.connect()
    356 
    357             # Intermediate frame without any preceding start of fragmentation
    358             # frame.
    359             client.send_frame_of_arbitrary_bytes('\x80\x80', '')
    360             client.assert_receive_close(
    361                 client_for_testing.STATUS_PROTOCOL_ERROR)
    362 
    363         self._run_hybi_test(test_function)
    364 
    365     def test_close_on_unsupported_frame(self):
    366         """Tests that the server sends a close frame with unsupported operation
    367         status code when the client sends data asking some operation that is
    368         not supported by the server.
    369         """
    370 
    371         def test_function(client):
    372             client.connect()
    373 
    374             # Text frame with RSV3 bit raised.
    375             client.send_frame_of_arbitrary_bytes('\x91\x80', '')
    376             client.assert_receive_close(
    377                 client_for_testing.STATUS_UNSUPPORTED_DATA)
    378 
    379         self._run_hybi_test(test_function)
    380 
    381     def test_close_on_invalid_frame(self):
    382         """Tests that the server sends a close frame with invalid frame payload
    383         data status code when the client sends an invalid frame like containing
    384         invalid UTF-8 character.
    385         """
    386 
    387         def test_function(client):
    388             client.connect()
    389 
    390             # Text frame with invalid UTF-8 string.
    391             client.send_message('\x80', raw=True)
    392             client.assert_receive_close(
    393                 client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA)
    394 
    395         self._run_hybi_test(test_function)
    396 
    397     def _run_hybi00_test(self, test_function):
    398         server = self._run_server()
    399         try:
    400             time.sleep(0.2)
    401 
    402             client = client_for_testing.create_client_hybi00(self._options)
    403             try:
    404                 test_function(client)
    405             finally:
    406                 client.close_socket()
    407         finally:
    408             self._kill_process(server.pid)
    409 
    410     def test_echo_hybi00(self):
    411         self._run_hybi00_test(_echo_check_procedure)
    412 
    413     def test_echo_server_close_hybi00(self):
    414         self._run_hybi00_test(_echo_check_procedure_with_goodbye)
    415 
    416     def _run_hixie75_test(self, test_function):
    417         server = self._run_server(allow_draft75=True)
    418         try:
    419             time.sleep(0.2)
    420 
    421             client = client_for_testing.create_client_hixie75(self._options)
    422             try:
    423                 test_function(client)
    424             finally:
    425                 client.close_socket()
    426         finally:
    427             self._kill_process(server.pid)
    428 
    429     def test_echo_hixie75(self):
    430         """Tests that the server can talk draft-hixie-thewebsocketprotocol-75
    431         protocol.
    432         """
    433 
    434         def test_function(client):
    435             client.connect()
    436 
    437             client.send_message('test')
    438             client.assert_receive('test')
    439 
    440         self._run_hixie75_test(test_function)
    441 
    442     def test_echo_server_close_hixie75(self):
    443         """Tests that the server can talk draft-hixie-thewebsocketprotocol-75
    444         protocol. At the end of message exchanging, the client sends a keyword
    445         message that requests the server to close the connection, and then
    446         checks if the connection is really closed.
    447         """
    448 
    449         def test_function(client):
    450             client.connect()
    451 
    452             client.send_message('test')
    453             client.assert_receive('test')
    454 
    455             client.send_message(_GOODBYE_MESSAGE)
    456             client.assert_receive(_GOODBYE_MESSAGE)
    457 
    458         self._run_hixie75_test(test_function)
    459 
    460     # TODO(toyoshim): Add tests to verify invalid absolute uri handling like
    461     # host unmatch, port unmatch and invalid port description (':' without port
    462     # number).
    463 
    464     def test_absolute_uri(self):
    465         """Tests absolute uri request."""
    466 
    467         options = self._options
    468         options.resource = 'ws://localhost:%d/echo' % options.server_port
    469         self._run_hybi_test_with_client_options(_echo_check_procedure, options)
    470 
    471     def test_origin_check(self):
    472         """Tests http fallback on origin check fail."""
    473 
    474         options = self._options
    475         options.resource = '/origin_check'
    476         # Server shows warning message for http 403 fallback. This warning
    477         # message is confusing. Following pipe disposes warning messages.
    478         self.server_stderr = subprocess.PIPE
    479         self._run_hybi_http_fallback_test(options, 403)
    480 
    481     def test_version_check(self):
    482         """Tests http fallback on version check fail."""
    483 
    484         options = self._options
    485         options.version = 99
    486         self.server_stderr = subprocess.PIPE
    487         self._run_hybi_http_fallback_test(options, 400)
    488 
    489     def _check_example_echo_client_result(
    490         self, expected, stdoutdata, stderrdata):
    491         actual = stdoutdata.decode("utf-8")
    492         if actual != expected:
    493             raise Exception('Unexpected result on example echo client: '
    494                             '%r (expected) vs %r (actual)' %
    495                             (expected, actual))
    496         if stderrdata is not None:
    497             raise Exception('Unexpected error message on example echo '
    498                             'client: %r' % stderrdata)
    499 
    500     def test_example_echo_client(self):
    501         """Tests that the echo_client.py example can talk with the server."""
    502 
    503         server = self._run_server()
    504         try:
    505             time.sleep(0.2)
    506 
    507             client_command = os.path.join(
    508                 self.top_dir, 'example', 'echo_client.py')
    509 
    510             args = [client_command,
    511                     '-p', str(self._options.server_port)]
    512             client = self._run_python_command(args, stdout=subprocess.PIPE)
    513             stdoutdata, stderrdata = client.communicate()
    514             expected = ('Send: Hello\n' 'Recv: Hello\n'
    515                 u'Send: \u65e5\u672c\n' u'Recv: \u65e5\u672c\n'
    516                 'Send close\n' 'Recv ack\n')
    517             self._check_example_echo_client_result(
    518                 expected, stdoutdata, stderrdata)
    519 
    520             # Process a big message for which extended payload length is used.
    521             # To handle extended payload length, ws_version attribute will be
    522             # accessed. This test checks that ws_version is correctly set.
    523             big_message = 'a' * 1024
    524             args = [client_command,
    525                     '-p', str(self._options.server_port),
    526                     '-m', big_message]
    527             client = self._run_python_command(args, stdout=subprocess.PIPE)
    528             stdoutdata, stderrdata = client.communicate()
    529             expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' %
    530                         (big_message, big_message))
    531             self._check_example_echo_client_result(
    532                 expected, stdoutdata, stderrdata)
    533         finally:
    534             self._kill_process(server.pid)
    535 
    536 
    537 if __name__ == '__main__':
    538     unittest.main()
    539 
    540 
    541 # vi:sts=4 sw=4 et
    542