1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Tests for L{twisted.web.resource}.
6"""
7
8from twisted.trial.unittest import TestCase
9
10from twisted.web.error import UnsupportedMethod
11from twisted.web.resource import (
12    NOT_FOUND, FORBIDDEN, Resource, ErrorPage, NoResource, ForbiddenResource,
13    getChildForRequest)
14from twisted.web.test.requesthelper import DummyRequest
15
16
17class ErrorPageTests(TestCase):
18    """
19    Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}.
20    """
21
22    errorPage = ErrorPage
23    noResource = NoResource
24    forbiddenResource = ForbiddenResource
25
26    def test_getChild(self):
27        """
28        The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
29        called on.
30        """
31        page = self.errorPage(321, "foo", "bar")
32        self.assertIdentical(page.getChild(b"name", object()), page)
33
34
35    def _pageRenderingTest(self, page, code, brief, detail):
36        request = DummyRequest([b''])
37        template = (
38            u"\n"
39            u"<html>\n"
40            u"  <head><title>%s - %s</title></head>\n"
41            u"  <body>\n"
42            u"    <h1>%s</h1>\n"
43            u"    <p>%s</p>\n"
44            u"  </body>\n"
45            u"</html>\n")
46        expected = template % (code, brief, brief, detail)
47        self.assertEqual(
48            page.render(request), expected.encode('utf-8'))
49        self.assertEqual(request.responseCode, code)
50        self.assertEqual(
51            request.outgoingHeaders,
52            {b'content-type': b'text/html; charset=utf-8'})
53
54
55    def test_errorPageRendering(self):
56        """
57        L{ErrorPage.render} returns a C{bytes} describing the error defined by
58        the response code and message passed to L{ErrorPage.__init__}.  It also
59        uses that response code to set the response code on the L{Request}
60        passed in.
61        """
62        code = 321
63        brief = "brief description text"
64        detail = "much longer text might go here"
65        page = self.errorPage(code, brief, detail)
66        self._pageRenderingTest(page, code, brief, detail)
67
68
69    def test_noResourceRendering(self):
70        """
71        L{NoResource} sets the HTTP I{NOT FOUND} code.
72        """
73        detail = "long message"
74        page = self.noResource(detail)
75        self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)
76
77
78    def test_forbiddenResourceRendering(self):
79        """
80        L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
81        """
82        detail = "longer message"
83        page = self.forbiddenResource(detail)
84        self._pageRenderingTest(page, FORBIDDEN, "Forbidden Resource", detail)
85
86
87
88class DynamicChild(Resource):
89    """
90    A L{Resource} to be created on the fly by L{DynamicChildren}.
91    """
92    def __init__(self, path, request):
93        Resource.__init__(self)
94        self.path = path
95        self.request = request
96
97
98
99class DynamicChildren(Resource):
100    """
101    A L{Resource} with dynamic children.
102    """
103    def getChild(self, path, request):
104        return DynamicChild(path, request)
105
106
107
108class BytesReturnedRenderable(Resource):
109    """
110    A L{Resource} with minimal capabilities to render a response.
111    """
112    def __init__(self, response):
113        """
114        @param response: A C{bytes} object giving the value to return from
115            C{render_GET}.
116        """
117        Resource.__init__(self)
118        self._response = response
119
120
121    def render_GET(self, request):
122        """
123        Render a response to a I{GET} request by returning a short byte string
124        to be written by the server.
125        """
126        return self._response
127
128
129
130class ImplicitAllowedMethods(Resource):
131    """
132    A L{Resource} which implicitly defines its allowed methods by defining
133    renderers to handle them.
134    """
135    def render_GET(self, request):
136        pass
137
138
139    def render_PUT(self, request):
140        pass
141
142
143
144class ResourceTests(TestCase):
145    """
146    Tests for L{Resource}.
147    """
148    def test_staticChildren(self):
149        """
150        L{Resource.putChild} adds a I{static} child to the resource.  That child
151        is returned from any call to L{Resource.getChildWithDefault} for the
152        child's path.
153        """
154        resource = Resource()
155        child = Resource()
156        sibling = Resource()
157        resource.putChild(b"foo", child)
158        resource.putChild(b"bar", sibling)
159        self.assertIdentical(
160            child, resource.getChildWithDefault(b"foo", DummyRequest([])))
161
162
163    def test_dynamicChildren(self):
164        """
165        L{Resource.getChildWithDefault} delegates to L{Resource.getChild} when
166        the requested path is not associated with any static child.
167        """
168        path = b"foo"
169        request = DummyRequest([])
170        resource = DynamicChildren()
171        child = resource.getChildWithDefault(path, request)
172        self.assertIsInstance(child, DynamicChild)
173        self.assertEqual(child.path, path)
174        self.assertIdentical(child.request, request)
175
176
177    def test_defaultHEAD(self):
178        """
179        When not otherwise overridden, L{Resource.render} treats a I{HEAD}
180        request as if it were a I{GET} request.
181        """
182        expected = b"insert response here"
183        request = DummyRequest([])
184        request.method = b'HEAD'
185        resource = BytesReturnedRenderable(expected)
186        self.assertEqual(expected, resource.render(request))
187
188
189    def test_explicitAllowedMethods(self):
190        """
191        The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
192        request method has a C{allowedMethods} attribute set to the value of the
193        C{allowedMethods} attribute of the L{Resource}, if it has one.
194        """
195        expected = [b'GET', b'HEAD', b'PUT']
196        resource = Resource()
197        resource.allowedMethods = expected
198        request = DummyRequest([])
199        request.method = b'FICTIONAL'
200        exc = self.assertRaises(UnsupportedMethod, resource.render, request)
201        self.assertEqual(set(expected), set(exc.allowedMethods))
202
203
204    def test_implicitAllowedMethods(self):
205        """
206        The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
207        request method has a C{allowedMethods} attribute set to a list of the
208        methods supported by the L{Resource}, as determined by the
209        I{render_}-prefixed methods which it defines, if C{allowedMethods} is
210        not explicitly defined by the L{Resource}.
211        """
212        expected = set([b'GET', b'HEAD', b'PUT'])
213        resource = ImplicitAllowedMethods()
214        request = DummyRequest([])
215        request.method = b'FICTIONAL'
216        exc = self.assertRaises(UnsupportedMethod, resource.render, request)
217        self.assertEqual(expected, set(exc.allowedMethods))
218
219
220
221
222class GetChildForRequestTests(TestCase):
223    """
224    Tests for L{getChildForRequest}.
225    """
226    def test_exhaustedPostPath(self):
227        """
228        L{getChildForRequest} returns whatever resource has been reached by the
229        time the request's C{postpath} is empty.
230        """
231        request = DummyRequest([])
232        resource = Resource()
233        result = getChildForRequest(resource, request)
234        self.assertIdentical(resource, result)
235
236
237    def test_leafResource(self):
238        """
239        L{getChildForRequest} returns the first resource it encounters with a
240        C{isLeaf} attribute set to C{True}.
241        """
242        request = DummyRequest([b"foo", b"bar"])
243        resource = Resource()
244        resource.isLeaf = True
245        result = getChildForRequest(resource, request)
246        self.assertIdentical(resource, result)
247
248
249    def test_postPathToPrePath(self):
250        """
251        As path segments from the request are traversed, they are taken from
252        C{postpath} and put into C{prepath}.
253        """
254        request = DummyRequest([b"foo", b"bar"])
255        root = Resource()
256        child = Resource()
257        child.isLeaf = True
258        root.putChild(b"foo", child)
259        self.assertIdentical(child, getChildForRequest(root, request))
260        self.assertEqual(request.prepath, [b"foo"])
261        self.assertEqual(request.postpath, [b"bar"])
262