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