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