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; charset=UTF-8")
94
95        # Server raises an exception if we don't start to read the data
96        response.read()
97
98    def test_get_css(self):
99        self.client.request("GET", "/pydoc.css")
100        response = self.client.getresponse()
101
102        self.assertEqual(response.status, 200)
103        self.assertEqual(response.getheader("Content-type"), "text/css; charset=UTF-8")
104
105        # Server raises an exception if we don't start to read the data
106        response.read()
107
108    def test_invalid_get_response(self):
109        self.client.request("GET", "/spam")
110        response = self.client.getresponse()
111
112        self.assertEqual(response.status, 404)
113        self.assertEqual(response.getheader("Content-type"), "text/plain")
114
115        response.read()
116
117    def test_lambda(self):
118        """Test that lambda functionality stays the same.  The output produced
119        currently is, I suspect invalid because of the unencoded brackets in the
120        HTML, "<lambda>".
121
122        The subtraction lambda method is tested.
123        """
124        self.client.request("GET", "/")
125        response = self.client.getresponse()
126
127        self.assertIn((b'<dl><dt><a name="-&lt;lambda&gt;"><strong>'
128                       b'&lt;lambda&gt;</strong></a>(x, y)</dt></dl>'),
129                      response.read())
130
131    @make_request_and_skipIf(sys.flags.optimize >= 2,
132                     "Docstrings are omitted with -O2 and above")
133    def test_autolinking(self):
134        """Test that the server correctly automatically wraps references to
135        PEPS and RFCs with links, and that it linkifies text starting with
136        http or ftp protocol prefixes.
137
138        The documentation for the "add" method contains the test material.
139        """
140        self.client.request("GET", "/")
141        response = self.client.getresponse().read()
142
143        self.assertIn(
144            (b'<dl><dt><a name="-add"><strong>add</strong></a>(x, y)</dt><dd>'
145             b'<tt>Add&nbsp;two&nbsp;instances&nbsp;together.&nbsp;This&nbsp;'
146             b'follows&nbsp;<a href="https://www.python.org/dev/peps/pep-0008/">'
147             b'PEP008</a>,&nbsp;but&nbsp;has&nbsp;nothing<br>\nto&nbsp;do&nbsp;'
148             b'with&nbsp;<a href="http://www.rfc-editor.org/rfc/rfc1952.txt">'
149             b'RFC1952</a>.&nbsp;Case&nbsp;should&nbsp;matter:&nbsp;pEp008&nbsp;'
150             b'and&nbsp;rFC1952.&nbsp;&nbsp;Things<br>\nthat&nbsp;start&nbsp;'
151             b'with&nbsp;http&nbsp;and&nbsp;ftp&nbsp;should&nbsp;be&nbsp;'
152             b'auto-linked,&nbsp;too:<br>\n<a href="http://google.com">'
153             b'http://google.com</a>.</tt></dd></dl>'), response)
154
155    @make_request_and_skipIf(sys.flags.optimize >= 2,
156                     "Docstrings are omitted with -O2 and above")
157    def test_system_methods(self):
158        """Test the presence of three consecutive system.* methods.
159
160        This also tests their use of parameter type recognition and the
161        systems related to that process.
162        """
163        self.client.request("GET", "/")
164        response = self.client.getresponse().read()
165
166        self.assertIn(
167            (b'<dl><dt><a name="-system.methodHelp"><strong>system.methodHelp'
168             b'</strong></a>(method_name)</dt><dd><tt><a href="#-system.method'
169             b'Help">system.methodHelp</a>(\'add\')&nbsp;=&gt;&nbsp;"Adds&nbsp;'
170             b'two&nbsp;integers&nbsp;together"<br>\n&nbsp;<br>\nReturns&nbsp;a'
171             b'&nbsp;string&nbsp;containing&nbsp;documentation&nbsp;for&nbsp;'
172             b'the&nbsp;specified&nbsp;method.</tt></dd></dl>\n<dl><dt><a name'
173             b'="-system.methodSignature"><strong>system.methodSignature</strong>'
174             b'</a>(method_name)</dt><dd><tt><a href="#-system.methodSignature">'
175             b'system.methodSignature</a>(\'add\')&nbsp;=&gt;&nbsp;[double,&nbsp;'
176             b'int,&nbsp;int]<br>\n&nbsp;<br>\nReturns&nbsp;a&nbsp;list&nbsp;'
177             b'describing&nbsp;the&nbsp;signature&nbsp;of&nbsp;the&nbsp;method.'
178             b'&nbsp;In&nbsp;the<br>\nabove&nbsp;example,&nbsp;the&nbsp;add&nbsp;'
179             b'method&nbsp;takes&nbsp;two&nbsp;integers&nbsp;as&nbsp;arguments'
180             b'<br>\nand&nbsp;returns&nbsp;a&nbsp;double&nbsp;result.<br>\n&nbsp;'
181             b'<br>\nThis&nbsp;server&nbsp;does&nbsp;NOT&nbsp;support&nbsp;system'
182             b'.methodSignature.</tt></dd></dl>'), response)
183
184    def test_autolink_dotted_methods(self):
185        """Test that selfdot values are made strong automatically in the
186        documentation."""
187        self.client.request("GET", "/")
188        response = self.client.getresponse()
189
190        self.assertIn(b"""Try&nbsp;self.<strong>add</strong>,&nbsp;too.""",
191                      response.read())
192
193    def test_annotations(self):
194        """ Test that annotations works as expected """
195        self.client.request("GET", "/")
196        response = self.client.getresponse()
197        docstring = (b'' if sys.flags.optimize >= 2 else
198                     b'<dd><tt>Use&nbsp;function&nbsp;annotations.</tt></dd>')
199        self.assertIn(
200            (b'<dl><dt><a name="-annotation"><strong>annotation</strong></a>'
201             b'(x: int)</dt>' + docstring + b'</dl>\n'
202             b'<dl><dt><a name="-method_annotation"><strong>'
203             b'method_annotation</strong></a>(x: bytes)</dt></dl>'),
204            response.read())
205
206    def test_server_title_escape(self):
207        # bpo-38243: Ensure that the server title and documentation
208        # are escaped for HTML.
209        self.serv.set_server_title('test_title<script>')
210        self.serv.set_server_documentation('test_documentation<script>')
211        self.assertEqual('test_title<script>', self.serv.server_title)
212        self.assertEqual('test_documentation<script>',
213                self.serv.server_documentation)
214
215        generated = self.serv.generate_html_documentation()
216        title = re.search(r'<title>(.+?)</title>', generated).group()
217        documentation = re.search(r'<p><tt>(.+?)</tt></p>', generated).group()
218        self.assertEqual('<title>Python: test_title&lt;script&gt;</title>', title)
219        self.assertEqual('<p><tt>test_documentation&lt;script&gt;</tt></p>', documentation)
220
221
222if __name__ == '__main__':
223    unittest.main()
224