1from xmlrpc.server import DocXMLRPCServer
2import http.client
3import re
4import sys
5import threading
6import unittest
7
8def make_request_and_skipIf(condition, reason):
9    # If we skip the test, we have to make a request because
10    # the server created in setUp blocks expecting one to come in.
11    if not condition:
12        return lambda func: func
13    def decorator(func):
14        def make_request_and_skip(self):
15            self.client.request("GET", "/")
16            self.client.getresponse()
17            raise unittest.SkipTest(reason)
18        return make_request_and_skip
19    return decorator
20
21
22def make_server():
23    serv = DocXMLRPCServer(("localhost", 0), logRequests=False)
24
25    try:
26        # Add some documentation
27        serv.set_server_title("DocXMLRPCServer Test Documentation")
28        serv.set_server_name("DocXMLRPCServer Test Docs")
29        serv.set_server_documentation(
30            "This is an XML-RPC server's documentation, but the server "
31            "can be used by POSTing to /RPC2. Try self.add, too.")
32
33        # Create and register classes and functions
34        class TestClass(object):
35            def test_method(self, arg):
36                """Test method's docs. This method truly does very little."""
37                self.arg = arg
38
39        serv.register_introspection_functions()
40        serv.register_instance(TestClass())
41
42        def add(x, y):
43            """Add two instances together. This follows PEP008, but has nothing
44            to do with RFC1952. Case should matter: pEp008 and rFC1952.  Things
45            that start with http and ftp should be auto-linked, too:
46            http://google.com.
47            """
48            return x + y
49
50        def annotation(x: int):
51            """ Use function annotations. """
52            return x
53
54        class ClassWithAnnotation:
55            def method_annotation(self, x: bytes):
56                return x.decode()
57
58        serv.register_function(add)
59        serv.register_function(lambda x, y: x-y)
60        serv.register_function(annotation)
61        serv.register_instance(ClassWithAnnotation())
62        return serv
63    except:
64        serv.server_close()
65        raise
66
67class DocXMLRPCHTTPGETServer(unittest.TestCase):
68    def setUp(self):
69        # Enable server feedback
70        DocXMLRPCServer._send_traceback_header = True
71
72        self.serv = make_server()
73        self.thread = threading.Thread(target=self.serv.serve_forever)
74        self.thread.start()
75
76        PORT = self.serv.server_address[1]
77        self.client = http.client.HTTPConnection("localhost:%d" % PORT)
78
79    def tearDown(self):
80        self.client.close()
81
82        # Disable server feedback
83        DocXMLRPCServer._send_traceback_header = False
84        self.serv.shutdown()
85        self.thread.join()
86        self.serv.server_close()
87
88    def test_valid_get_response(self):
89        self.client.request("GET", "/")
90        response = self.client.getresponse()
91
92        self.assertEqual(response.status, 200)
93        self.assertEqual(response.getheader("Content-type"), "text/html")
94
95        # Server raises an exception if we don't start to read the data
96        response.read()
97
98    def test_invalid_get_response(self):
99        self.client.request("GET", "/spam")
100        response = self.client.getresponse()
101
102        self.assertEqual(response.status, 404)
103        self.assertEqual(response.getheader("Content-type"), "text/plain")
104
105        response.read()
106
107    def test_lambda(self):
108        """Test that lambda functionality stays the same.  The output produced
109        currently is, I suspect invalid because of the unencoded brackets in the
110        HTML, "<lambda>".
111
112        The subtraction lambda method is tested.
113        """
114        self.client.request("GET", "/")
115        response = self.client.getresponse()
116
117        self.assertIn((b'<dl><dt><a name="-&lt;lambda&gt;"><strong>'
118                       b'&lt;lambda&gt;</strong></a>(x, y)</dt></dl>'),
119                      response.read())
120
121    @make_request_and_skipIf(sys.flags.optimize >= 2,
122                     "Docstrings are omitted with -O2 and above")
123    def test_autolinking(self):
124        """Test that the server correctly automatically wraps references to
125        PEPS and RFCs with links, and that it linkifies text starting with
126        http or ftp protocol prefixes.
127
128        The documentation for the "add" method contains the test material.
129        """
130        self.client.request("GET", "/")
131        response = self.client.getresponse().read()
132
133        self.assertIn(
134            (b'<dl><dt><a name="-add"><strong>add</strong></a>(x, y)</dt><dd>'
135             b'<tt>Add&nbsp;two&nbsp;instances&nbsp;together.&nbsp;This&nbsp;'
136             b'follows&nbsp;<a href="http://www.python.org/dev/peps/pep-0008/">'
137             b'PEP008</a>,&nbsp;but&nbsp;has&nbsp;nothing<br>\nto&nbsp;do&nbsp;'
138             b'with&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1952.txt">'
139             b'RFC1952</a>.&nbsp;Case&nbsp;should&nbsp;matter:&nbsp;pEp008&nbsp;'
140             b'and&nbsp;rFC1952.&nbsp;&nbsp;Things<br>\nthat&nbsp;start&nbsp;'
141             b'with&nbsp;http&nbsp;and&nbsp;ftp&nbsp;should&nbsp;be&nbsp;'
142             b'auto-linked,&nbsp;too:<br>\n<a href="http://google.com">'
143             b'http://google.com</a>.</tt></dd></dl>'), response)
144
145    @make_request_and_skipIf(sys.flags.optimize >= 2,
146                     "Docstrings are omitted with -O2 and above")
147    def test_system_methods(self):
148        """Test the presence of three consecutive system.* methods.
149
150        This also tests their use of parameter type recognition and the
151        systems related to that process.
152        """
153        self.client.request("GET", "/")
154        response = self.client.getresponse().read()
155
156        self.assertIn(
157            (b'<dl><dt><a name="-system.methodHelp"><strong>system.methodHelp'
158             b'</strong></a>(method_name)</dt><dd><tt><a href="#-system.method'
159             b'Help">system.methodHelp</a>(\'add\')&nbsp;=&gt;&nbsp;"Adds&nbsp;'
160             b'two&nbsp;integers&nbsp;together"<br>\n&nbsp;<br>\nReturns&nbsp;a'
161             b'&nbsp;string&nbsp;containing&nbsp;documentation&nbsp;for&nbsp;'
162             b'the&nbsp;specified&nbsp;method.</tt></dd></dl>\n<dl><dt><a name'
163             b'="-system.methodSignature"><strong>system.methodSignature</strong>'
164             b'</a>(method_name)</dt><dd><tt><a href="#-system.methodSignature">'
165             b'system.methodSignature</a>(\'add\')&nbsp;=&gt;&nbsp;[double,&nbsp;'
166             b'int,&nbsp;int]<br>\n&nbsp;<br>\nReturns&nbsp;a&nbsp;list&nbsp;'
167             b'describing&nbsp;the&nbsp;signature&nbsp;of&nbsp;the&nbsp;method.'
168             b'&nbsp;In&nbsp;the<br>\nabove&nbsp;example,&nbsp;the&nbsp;add&nbsp;'
169             b'method&nbsp;takes&nbsp;two&nbsp;integers&nbsp;as&nbsp;arguments'
170             b'<br>\nand&nbsp;returns&nbsp;a&nbsp;double&nbsp;result.<br>\n&nbsp;'
171             b'<br>\nThis&nbsp;server&nbsp;does&nbsp;NOT&nbsp;support&nbsp;system'
172             b'.methodSignature.</tt></dd></dl>'), response)
173
174    def test_autolink_dotted_methods(self):
175        """Test that selfdot values are made strong automatically in the
176        documentation."""
177        self.client.request("GET", "/")
178        response = self.client.getresponse()
179
180        self.assertIn(b"""Try&nbsp;self.<strong>add</strong>,&nbsp;too.""",
181                      response.read())
182
183    def test_annotations(self):
184        """ Test that annotations works as expected """
185        self.client.request("GET", "/")
186        response = self.client.getresponse()
187        docstring = (b'' if sys.flags.optimize >= 2 else
188                     b'<dd><tt>Use&nbsp;function&nbsp;annotations.</tt></dd>')
189        self.assertIn(
190            (b'<dl><dt><a name="-annotation"><strong>annotation</strong></a>'
191             b'(x: int)</dt>' + docstring + b'</dl>\n'
192             b'<dl><dt><a name="-method_annotation"><strong>'
193             b'method_annotation</strong></a>(x: bytes)</dt></dl>'),
194            response.read())
195
196    def test_server_title_escape(self):
197        # bpo-38243: Ensure that the server title and documentation
198        # are escaped for HTML.
199        self.serv.set_server_title('test_title<script>')
200        self.serv.set_server_documentation('test_documentation<script>')
201        self.assertEqual('test_title<script>', self.serv.server_title)
202        self.assertEqual('test_documentation<script>',
203                self.serv.server_documentation)
204
205        generated = self.serv.generate_html_documentation()
206        title = re.search(r'<title>(.+?)</title>', generated).group()
207        documentation = re.search(r'<p><tt>(.+?)</tt></p>', generated).group()
208        self.assertEqual('<title>Python: test_title&lt;script&gt;</title>', title)
209        self.assertEqual('<p><tt>test_documentation&lt;script&gt;</tt></p>', documentation)
210
211
212if __name__ == '__main__':
213    unittest.main()
214