1"""
2Tests for on-the-fly content compression encoding.
3"""
4from StringIO import StringIO
5from gzip import GzipFile
6
7from zope.interface import implements
8
9from twisted.trial.unittest import TestCase
10from twisted.internet.defer import succeed
11
12from nevow.inevow import IResource, IRequest
13from nevow.testutil import FakeRequest
14from nevow.context import RequestContext
15from nevow.appserver import errorMarker
16from nevow.rend import NotFound
17from nevow.compression import CompressingResourceWrapper, CompressingRequestWrapper
18from nevow.compression import parseAcceptEncoding, _ProxyDescriptor
19
20
21
22class HeaderTests(TestCase):
23    """
24    Tests for header parsing.
25    """
26    def test_parseAcceptEncoding(self):
27        """
28        Test the parsing of a variety of Accept-Encoding field values.
29        """
30        cases = [
31            ('compress, gzip',
32             {'compress': 1.0, 'gzip': 1.0, 'identity': 0.0001}),
33            ('',
34             {'identity': 0.0001}),
35            ('*',
36             {'*': 1}),
37            ('compress;q=0.5, gzip;q=1.0',
38             {'compress': 0.5, 'gzip': 1.0, 'identity': 0.0001}),
39            ('gzip;q=1.0, identity;q=0.5, *;q=0',
40             {'gzip': 1.0, 'identity': 0.5, '*': 0})
41            ]
42
43        for value, result in cases:
44            self.assertEqual(parseAcceptEncoding(value), result, msg='error parsing %r' % value)
45
46
47
48class _Dummy(object):
49    """
50    Dummy object just to get an instance dict.
51    """
52
53
54
55class _Wrapper(object):
56    """
57    Test wrapper.
58    """
59    x = _ProxyDescriptor('x')
60    y = _ProxyDescriptor('y')
61
62    def __init__(self, underlying):
63        self.underlying = underlying
64
65
66
67class ProxyDescriptorTests(TestCase):
68    """
69    Tests for L{_ProxyDescriptor}.
70    """
71    def setUp(self):
72        """
73        Set up a dummy object and a wrapper for it.
74        """
75        self.dummy = _Dummy()
76        self.dummy.x = object()
77        self.dummy.y = object()
78        self.wrapper = _Wrapper(self.dummy)
79
80
81    def test_proxyGet(self):
82        """
83        Getting a proxied attribute should retrieve the underlying attribute.
84        """
85        self.assertIdentical(self.wrapper.x, self.dummy.x)
86        self.assertIdentical(self.wrapper.y, self.dummy.y)
87        self.dummy.x = object()
88        self.assertIdentical(self.wrapper.x, self.dummy.x)
89
90
91    def test_proxyClassGet(self):
92        """
93        Getting a proxied attribute from the class should just retrieve the
94        descriptor.
95        """
96        self.assertIdentical(_Wrapper.x, _Wrapper.__dict__['x'])
97
98
99    def test_proxySet(self):
100        """
101        Setting a proxied attribute should set the underlying attribute.
102        """
103        self.wrapper.x = object()
104        self.assertIdentical(self.dummy.x, self.wrapper.x)
105        self.wrapper.y = 5
106        self.assertEqual(self.dummy.y, 5)
107
108
109    def test_proxyDelete(self):
110        """
111        Deleting a proxied attribute should delete the underlying attribute.
112        """
113        self.assertTrue(hasattr(self.dummy, 'x'))
114        del self.wrapper.x
115        self.assertFalse(hasattr(self.dummy, 'x'))
116
117
118
119class RequestWrapperTests(TestCase):
120    """
121    Tests for L{CompressingRequestWrapper}.
122    """
123    def setUp(self):
124        """
125        Wrap a request fake to test the wrapper.
126        """
127        self.request = FakeRequest()
128        self.wrapper = CompressingRequestWrapper(self.request)
129
130
131    def test_attributes(self):
132        """
133        Attributes on the wrapper should be forwarded to the underlying
134        request.
135        """
136        attributes = ['method', 'uri', 'path', 'args', 'requestHeaders']
137        for attrName in attributes:
138            self.assertIdentical(getattr(self.wrapper, attrName),
139                                 getattr(self.request, attrName))
140
141
142    def test_missingAttributes(self):
143        """
144        Attributes that are not part of the interfaces being proxied should not
145        be proxied.
146        """
147        self.assertRaises(AttributeError, getattr, self.wrapper, 'doesntexist')
148        self.request._privateTestAttribute = 42
149        self.assertRaises(AttributeError, getattr, self.wrapper, '_privateTestAttribute')
150
151
152    def test_contentLength(self):
153        """
154        Content-Length header should be discarded when compression is in use.
155        """
156        self.assertFalse(
157            self.request.responseHeaders.hasHeader('content-length'))
158        self.wrapper.setHeader('content-length', 1234)
159        self.assertFalse(
160            self.request.responseHeaders.hasHeader('content-length'))
161
162        self.request.setHeader('content-length', 1234)
163        self.wrapper = CompressingRequestWrapper(self.request)
164        self.assertFalse(
165            self.request.responseHeaders.hasHeader('content-length'))
166
167
168    def test_responseHeaders(self):
169        """
170        Content-Encoding header should be set appropriately.
171        """
172        self.assertEqual(
173            self.request.responseHeaders.getRawHeaders('content-encoding'),
174            ['gzip'])
175
176
177    def test_lazySetup(self):
178        """
179        The gzip prelude should only be written once real data is written.
180
181        This is necessary to avoid terminating the header too quickly.
182        """
183        self.assertEqual(self.request.accumulator, '')
184        self.wrapper.write('foo')
185        self.assertNotEqual(self.request.accumulator, '')
186
187
188    def _ungzip(self, data):
189        """
190        Un-gzip some data.
191        """
192        s = StringIO(data)
193        return GzipFile(fileobj=s, mode='rb').read()
194
195
196    def test_encoding(self):
197        """
198        Response content should be written out in compressed format.
199        """
200        self.wrapper.write('foo')
201        self.wrapper.write('bar')
202        self.wrapper.finishRequest(True)
203        self.assertEqual(self._ungzip(self.request.accumulator), 'foobar')
204
205
206    def test_finish(self):
207        """
208        Calling C{finishRequest()} on the wrapper should cause the underlying
209        implementation to be called.
210        """
211        self.wrapper.finishRequest(True)
212        self.assertTrue(self.request.finished)
213
214
215
216class TestResource(object):
217    """
218    L{IResource} implementation for testing.
219
220    @ivar lastRequest: The last request we were rendered with.
221    @type lastRequest: L{IRequest} or C{None}
222    @ivar segments: The segments we were constructed with.
223    @type segments: C{list}
224    @ivar html: The data to return from C{renderHTTP}.
225    """
226    implements(IResource)
227
228    lastRequest = None
229
230    def __init__(self, segments=[], html='o hi'):
231        self.segments = segments
232        self.html = html
233
234
235    def locateChild(self, ctx, segments):
236        """
237        Construct a new resource of our type.
238
239        We hand out child resources for any segments, as this is the simplest
240        thing to do.
241        """
242        return type(self)(segments), []
243
244
245    def renderHTTP(self, ctx):
246        """
247        Stash the request for later inspection.
248        """
249        self.lastRequest = IRequest(ctx)
250        return self.html
251
252
253
254class TestChildlessResource(object):
255    """
256    L{IResource} implementation with no children.
257    """
258    implements(IResource)
259
260    def locateChild(self, ctx, segments):
261        """
262        Always return C{NotFound}.
263        """
264        return NotFound
265
266
267
268class TestDeferredResource(object):
269    """
270    L{IResource} implementation with children.
271    """
272    implements(IResource)
273
274    def locateChild(self, ctx, segments):
275        """
276        Construct a new resource of our type.
277
278        We hand out child resources for any segments, but the resource itself
279        is wrapped in a deferred.
280        """
281        return succeed(type(self)()), []
282
283
284
285class TestResourceWrapper(CompressingResourceWrapper):
286    """
287    Subclass for testing purposes, just to create a new type.
288    """
289
290
291class TestBrokenResource(object):
292    """
293    L{IResource} implementation that returns garbage from C{locateChild}.
294    """
295    implements(IResource)
296
297    def locateChild(self, ctx, segments):
298        """
299        Return some garbage.
300        """
301        return 42
302
303
304
305class ResourceWrapper(TestCase):
306    """
307    Tests for L{CompressingResourceWrapper}.
308
309    @ivar resource: The underlying resource for testing.
310    @type resource: L{TestResource}
311    @ivar wrapped: The wrapped resource.
312    @type wrapped: L{CompressingResourceWrapper}
313    @ivar request: A fake request.
314    @type request: L{FakeRequest}
315    @ivar ctx: A dummy context.
316    @type ctx: L{RequestContext}
317    """
318    def setUp(self):
319        self.resource = TestResource()
320        self.wrapped = CompressingResourceWrapper(self.resource)
321        self.request = FakeRequest()
322        self.ctx = RequestContext(tag=self.request)
323
324
325    def test_rendering(self):
326        """
327        Rendering a wrapped resource renders the underlying resource with a
328        wrapped request if compression is available.
329        """
330        self.wrapped.canCompress = lambda req: True
331        self.wrapped.renderHTTP(self.ctx)
332        self.assertEqual(type(self.resource.lastRequest), CompressingRequestWrapper)
333        self.assertIdentical(self.resource.lastRequest.underlying, self.request)
334
335
336    def test_renderingUnwrapped(self):
337        """
338        Rendering a wrapped resource renders the underlying resource with an
339        unwrapped request if compression is not available.
340        """
341        self.wrapped.canCompress = lambda req: False
342        self.wrapped.renderHTTP(self.ctx)
343        self.assertIdentical(self.resource.lastRequest, self.request)
344
345
346    def test_awfulHack(self):
347        """
348        Rendering a wrapped resource should finish off the request, and return
349        a special sentinel value to prevent the Site machinery from trying to
350        finish it again.
351        """
352        def _cbCheckReturn(result):
353            self.rendered = True
354            self.assertIdentical(result, errorMarker)
355            self.assertTrue(self.request.finished)
356
357        self.rendered = False
358        self.wrapped.canCompress = lambda req: True
359        self.wrapped.renderHTTP(self.ctx).addCallback(_cbCheckReturn)
360        # The callback should run synchronously
361        self.assertTrue(self.rendered)
362
363
364    def test_rendering(self):
365        """
366        Returning something other than C{str} causes the value to be passed
367        through.
368        """
369        def _cbResult(result):
370            self.result = result
371
372        marker = object()
373        self.resource.html = marker
374        self.wrapped.canCompress = lambda req: True
375        self.wrapped.renderHTTP(self.ctx).addCallback(_cbResult)
376        self.assertIdentical(marker, self.result)
377
378
379    def _cbCheckChild(self, result):
380        """
381        Check that the child resource is wrapped correctly.
382        """
383        self.checked = True
384
385
386    def _locateChild(self, resource, segments):
387        """
388        Helper function for retrieving a child synchronously.
389        """
390        def _cbGotChild(result):
391            self.gotChild = True
392            self.result = result
393
394        def _ebChild(f):
395            self.gotChild = 'error'
396            self.f = f
397
398        self.gotChild = False
399        resource.locateChild(None, segments).addCallbacks(_cbGotChild, _ebChild)
400        self.assertTrue(self.gotChild)
401        if self.gotChild == 'error':
402            self.f.raiseException()
403        return self.result
404
405
406    def test_wrapChildren(self):
407        """
408        Any children of the wrapped resource should also be wrapped.
409        """
410        self.checked = False
411        child, segments = self._locateChild(self.wrapped, ['some', 'child', 'segments'])
412        self.assertIdentical(type(child), type(self.wrapped))
413        self.assertEqual(child.underlying.segments, ['some', 'child', 'segments'])
414
415
416    def test_wrapChildrenSubclass(self):
417        """
418        The request wrapper should wrap children with the same type.
419        """
420        self.wrapped = TestResourceWrapper(self.resource)
421        self.test_wrapChildren()
422
423
424    def test_childNotFound(self):
425        """
426        The request wrapper should pass C{NotFound} through.
427        """
428        wrapped = CompressingResourceWrapper(TestChildlessResource())
429        result = self._locateChild(wrapped, ['foo'])
430        self.assertEqual(result, NotFound)
431
432
433    def test_deferredChild(self):
434        """
435        The wrapper should deal with a resource wrapped in a deferred returned
436        from locateChild.
437        """
438        wrapped = CompressingResourceWrapper(TestDeferredResource())
439        child, segments = self._locateChild(wrapped, ['foo'])
440        self.assertEqual(type(child.underlying), TestDeferredResource)
441        self.assertEqual(segments, [])
442
443
444    def test_brokenChild(self):
445        """
446        C{ValueError} should be raised if the underlying C{locateChild} returns
447        something bogus.
448        """
449        wrapped = CompressingResourceWrapper(TestBrokenResource())
450        self.assertRaises(ValueError, self._locateChild, wrapped, ['foo'])
451
452
453    def test_negotiation(self):
454        """
455        Request wrapping should only occur when the client has indicated they
456        can accept compression.
457        """
458        self.assertFalse(self.wrapped.canCompress(self.request))
459
460        self.request.requestHeaders.setRawHeaders(
461            'accept-encoding', ['foo;q=1.0, bar;q=0.5, baz'])
462        self.assertFalse(self.wrapped.canCompress(self.request))
463
464        self.request.requestHeaders.setRawHeaders('accept-encoding', ['gzip'])
465        self.assertTrue(self.wrapped.canCompress(self.request))
466
467        self.request.requestHeaders.setRawHeaders(
468            'accept-encoding', ['gzip;q=0.5'])
469        self.assertTrue(self.wrapped.canCompress(self.request))
470
471        self.request.requestHeaders.setRawHeaders(
472            'accept-encoding', ['gzip;q=0'])
473        self.assertFalse(self.wrapped.canCompress(self.request))
474