comparison test/test_cgi.py @ 8185:e84d4585b16d

fix(web): issue2551356. Add etag header for not-modified (304) request. When a 304 is returned to a conditional request for a static file, print an ETag for the response. ETag was always sent with a 200 response. This also adds initial support for if-none-match conditional requests for static files. Changes: Refactors the if-modified-since code out to a method. It moves a file stat call from serve_static_file to _serve_file so that an etag can be generated by both serve_static_file and serve_file which call _serve_file. Tests added. This does not test the codepath where serve_file pulls content from the database rather than from a local file on disk. Test mocking _serve_file changed to account for 5th argument to serve_file BREAKING CHANGE: function signature for client.py-Client::_serve_file() now has 5 not 4 parameters (added etag param). Since this is a "hidden" method I am not too worried about it.
author John Rouillard <rouilj@ieee.org>
date Tue, 10 Dec 2024 16:06:13 -0500
parents 603aa730b067
children 0242cf22ef74
comparison
equal deleted inserted replaced
8184:53dba022d4cd 8185:e84d4585b16d
16 16
17 from os.path import normpath 17 from os.path import normpath
18 18
19 from roundup.anypy.cgi_ import cgi 19 from roundup.anypy.cgi_ import cgi
20 from roundup.cgi import client, actions, exceptions 20 from roundup.cgi import client, actions, exceptions
21 from roundup.cgi.exceptions import FormError, NotFound, Redirect 21 from roundup.cgi.exceptions import FormError, NotFound, Redirect, NotModified
22 from roundup.exceptions import UsageError, Reject 22 from roundup.exceptions import UsageError, Reject
23 from roundup.cgi.templating import HTMLItem, HTMLRequest, NoTemplate 23 from roundup.cgi.templating import HTMLItem, HTMLRequest, NoTemplate
24 from roundup.cgi.templating import HTMLProperty, _HTMLItem, anti_csrf_nonce 24 from roundup.cgi.templating import HTMLProperty, _HTMLItem, anti_csrf_nonce
25 from roundup.cgi.templating import TemplatingUtils 25 from roundup.cgi.templating import TemplatingUtils
26 from roundup.cgi.form_parser import FormParser 26 from roundup.cgi.form_parser import FormParser
1940 'REQUEST_METHOD':'POST'}, db_test_base.makeForm(form)) 1940 'REQUEST_METHOD':'POST'}, db_test_base.makeForm(form))
1941 cl.classname = classname 1941 cl.classname = classname
1942 if nodeid is not None: 1942 if nodeid is not None:
1943 cl.nodeid = nodeid 1943 cl.nodeid = nodeid
1944 cl.db = self.db 1944 cl.db = self.db
1945 cl.request = MockNull()
1945 cl.db.Otk = cl.db.getOTKManager() 1946 cl.db.Otk = cl.db.getOTKManager()
1946 #cl.db.Otk = MockNull() 1947 #cl.db.Otk = MockNull()
1947 #cl.db.Otk.data = {} 1948 #cl.db.Otk.data = {}
1948 #cl.db.Otk.getall = self.data_get 1949 #cl.db.Otk.getall = self.data_get
1949 #cl.db.Otk.set = self.data_set 1950 #cl.db.Otk.set = self.data_set
2380 2381
2381 # clean up from email log 2382 # clean up from email log
2382 if os.path.exists(SENDMAILDEBUG): 2383 if os.path.exists(SENDMAILDEBUG):
2383 os.remove(SENDMAILDEBUG) 2384 os.remove(SENDMAILDEBUG)
2384 2385
2386 def testserve_static_files_cache_headers(self):
2387 """Note for headers the real headers class is case
2388 insensitive.
2389 """
2390 # make a client instance
2391 cl = self._make_client({})
2392 # Make local copy in cl to not modify value in class
2393 cl.Cache_Control = copy.copy (cl.Cache_Control)
2394
2395 # TEMPLATES dir is searched by default. So this file exists.
2396 # Check the returned values.
2397 cl.serve_static_file("style.css")
2398
2399 # gather the conditional request headers from the 200 response
2400 inm = cl.additional_headers['ETag']
2401 ims = cl.additional_headers['Last-Modified']
2402
2403
2404 # loop over all header value possibilities that will
2405 # result in not modified.
2406 for headers in [
2407 {'if-none-match' : inm},
2408 {'if-modified-since' : ims},
2409 {'if-none-match' : inm, 'if-modified-since' : ims },
2410 {'if-none-match' : inm, 'if-modified-since' : "fake" },
2411 {'if-none-match' : "fake", 'if-modified-since' : ims },
2412 ]:
2413 print(headers)
2414
2415 # Request same file with if-modified-since header
2416 # expect NotModified with same ETag and Last-Modified headers.
2417 cl.request.headers = headers
2418 cl.response_code = None
2419 cl.additional_headers = {}
2420
2421 with self.assertRaises(NotModified) as cm:
2422 cl.serve_static_file("style.css")
2423
2424 self.assertEqual(cm.exception.args, ())
2425
2426 self.assertEqual(cl.response_code, None)
2427 self.assertEqual(cl.additional_headers['ETag'], inm)
2428 self.assertEqual(cl.additional_headers['Last-Modified'], ims)
2429
2430
2431 ## run two cases that should not return NotModified
2432 for headers in [
2433 {},
2434 {'if-none-match' : "fake", 'if-modified-since' : "fake" },
2435 ]:
2436 cl.request.headers = headers
2437 cl.response_code = None
2438 cl.additional_headers = {}
2439
2440 cl.serve_static_file("style.css")
2441
2442 self.assertEqual(cl.response_code, None)
2443 self.assertEqual(cl.additional_headers['ETag'], inm)
2444 self.assertEqual(cl.additional_headers['Last-Modified'], ims)
2445
2446 ## test pure cgi case
2447 # headers attribute does not exist
2448 cl.request = None
2449 cl.response_code = None
2450 cl.additional_headers = {}
2451
2452 cl.env["HTTP_IF_MODIFIED_SINCE"] = ims
2453
2454 with self.assertRaises(NotModified) as cm:
2455 cl.serve_static_file("style.css")
2456
2457 self.assertEqual(cm.exception.args, ())
2458
2459 self.assertEqual(cl.response_code, None)
2460 self.assertEqual(cl.additional_headers['ETag'], inm)
2461 self.assertEqual(cl.additional_headers['Last-Modified'], ims)
2462
2385 def testserve_static_files(self): 2463 def testserve_static_files(self):
2386 # make a client instance 2464 # make a client instance
2387 cl = self._make_client({}) 2465 cl = self._make_client({})
2388 # Make local copy in cl to not modify value in class 2466 # Make local copy in cl to not modify value in class
2389 cl.Cache_Control = copy.copy (cl.Cache_Control) 2467 cl.Cache_Control = copy.copy (cl.Cache_Control)
2390 2468
2391 # hijack _serve_file so I can see what is found 2469 # hijack _serve_file so I can see what is found
2392 output = [] 2470 output = []
2393 def my_serve_file(a, b, c, d): 2471 def my_serve_file(a, b, c, d, e):
2394 output.append((a,b,c,d)) 2472 output.append((a,b,c,d,e))
2395 cl._serve_file = my_serve_file 2473 cl._serve_file = my_serve_file
2396 2474
2397 # check case where file is not found. 2475 # check case where file is not found.
2398 self.assertRaises(NotFound, 2476 self.assertRaises(NotFound,
2399 cl.serve_static_file,"missing.css") 2477 cl.serve_static_file,"missing.css")
2400 2478
2401 # TEMPLATES dir is searched by default. So this file exists. 2479 # TEMPLATES dir is searched by default. So this file exists.
2402 # Check the returned values. 2480 # Check the returned values.
2403 cl.serve_static_file("issue.index.html") 2481 cl.serve_static_file("issue.index.html")
2404 self.assertEqual(output[0][1], "text/html") 2482 print(output)
2405 self.assertEqual(output[0][3], 2483 self.assertEqual(output[0][2], "text/html")
2484 self.assertEqual(output[0][4],
2406 normpath('_test_cgi_form/html/issue.index.html')) 2485 normpath('_test_cgi_form/html/issue.index.html'))
2407 del output[0] # reset output buffer 2486 del output[0] # reset output buffer
2408 2487
2409 # stop searching TEMPLATES for the files. 2488 # stop searching TEMPLATES for the files.
2410 cl.instance.config['STATIC_FILES'] = '-' 2489 cl.instance.config['STATIC_FILES'] = '-'
2413 cl.serve_static_file,"issue.index.html") 2492 cl.serve_static_file,"issue.index.html")
2414 2493
2415 # explicitly allow html directory 2494 # explicitly allow html directory
2416 cl.instance.config['STATIC_FILES'] = 'html -' 2495 cl.instance.config['STATIC_FILES'] = 'html -'
2417 cl.serve_static_file("issue.index.html") 2496 cl.serve_static_file("issue.index.html")
2418 self.assertEqual(output[0][1], "text/html") 2497 self.assertEqual(output[0][2], "text/html")
2419 self.assertEqual(output[0][3], 2498 self.assertEqual(output[0][4],
2420 normpath('_test_cgi_form/html/issue.index.html')) 2499 normpath('_test_cgi_form/html/issue.index.html'))
2421 del output[0] # reset output buffer 2500 del output[0] # reset output buffer
2422 2501
2423 # set the list of files and do not look at the templates directory 2502 # set the list of files and do not look at the templates directory
2424 cl.instance.config['STATIC_FILES'] = 'detectors extensions - ' 2503 cl.instance.config['STATIC_FILES'] = 'detectors extensions - '
2425 2504
2426 # find file in first directory 2505 # find file in first directory
2427 cl.serve_static_file("messagesummary.py") 2506 cl.serve_static_file("messagesummary.py")
2428 self.assertEqual(output[0][1], "text/x-python") 2507 self.assertEqual(output[0][2], "text/x-python")
2429 self.assertEqual(output[0][3], 2508 self.assertEqual(output[0][4],
2430 normpath( "_test_cgi_form/detectors/messagesummary.py")) 2509 normpath( "_test_cgi_form/detectors/messagesummary.py"))
2431 del output[0] # reset output buffer 2510 del output[0] # reset output buffer
2432 2511
2433 # find file in second directory 2512 # find file in second directory
2434 cl.serve_static_file("README.txt") 2513 cl.serve_static_file("README.txt")
2435 self.assertEqual(output[0][1], "text/plain") 2514 self.assertEqual(output[0][2], "text/plain")
2436 self.assertEqual(output[0][3], 2515 self.assertEqual(output[0][4],
2437 normpath("_test_cgi_form/extensions/README.txt")) 2516 normpath("_test_cgi_form/extensions/README.txt"))
2438 del output[0] # reset output buffer 2517 del output[0] # reset output buffer
2439 2518
2440 # make sure an embedded - ends the searching. 2519 # make sure an embedded - ends the searching.
2441 cl.instance.config['STATIC_FILES'] = ' detectors - extensions ' 2520 cl.instance.config['STATIC_FILES'] = ' detectors - extensions '
2446 2525
2447 # create an empty README.txt in the first directory 2526 # create an empty README.txt in the first directory
2448 f = open('_test_cgi_form/detectors/README.txt', 'a').close() 2527 f = open('_test_cgi_form/detectors/README.txt', 'a').close()
2449 # find file now in first directory 2528 # find file now in first directory
2450 cl.serve_static_file("README.txt") 2529 cl.serve_static_file("README.txt")
2451 self.assertEqual(output[0][1], "text/plain") 2530 self.assertEqual(output[0][2], "text/plain")
2452 self.assertEqual(output[0][3], 2531 self.assertEqual(output[0][4],
2453 normpath("_test_cgi_form/detectors/README.txt")) 2532 normpath("_test_cgi_form/detectors/README.txt"))
2454 del output[0] # reset output buffer 2533 del output[0] # reset output buffer
2455 2534
2456 cl.instance.config['STATIC_FILES'] = ' detectors extensions ' 2535 cl.instance.config['STATIC_FILES'] = ' detectors extensions '
2457 # make sure lack of trailing - allows searching TEMPLATES 2536 # make sure lack of trailing - allows searching TEMPLATES
2458 cl.serve_static_file("issue.index.html") 2537 cl.serve_static_file("issue.index.html")
2459 self.assertEqual(output[0][1], "text/html") 2538 self.assertEqual(output[0][2], "text/html")
2460 self.assertEqual(output[0][3], 2539 self.assertEqual(output[0][4],
2461 normpath("_test_cgi_form/html/issue.index.html")) 2540 normpath("_test_cgi_form/html/issue.index.html"))
2462 del output[0] # reset output buffer 2541 del output[0] # reset output buffer
2463 2542
2464 # Make STATIC_FILES a single element. 2543 # Make STATIC_FILES a single element.
2465 cl.instance.config['STATIC_FILES'] = 'detectors' 2544 cl.instance.config['STATIC_FILES'] = 'detectors'
2466 # find file now in first directory 2545 # find file now in first directory
2467 cl.serve_static_file("messagesummary.py") 2546 cl.serve_static_file("messagesummary.py")
2468 self.assertEqual(output[0][1], "text/x-python") 2547 self.assertEqual(output[0][2], "text/x-python")
2469 self.assertEqual(output[0][3], 2548 self.assertEqual(output[0][4],
2470 normpath("_test_cgi_form/detectors/messagesummary.py")) 2549 normpath("_test_cgi_form/detectors/messagesummary.py"))
2471 del output[0] # reset output buffer 2550 del output[0] # reset output buffer
2472 2551
2473 # make sure files found in subdirectory 2552 # make sure files found in subdirectory
2474 os.mkdir('_test_cgi_form/detectors/css') 2553 os.mkdir('_test_cgi_form/detectors/css')
2475 f = open('_test_cgi_form/detectors/css/README.css', 'a').close() 2554 f = open('_test_cgi_form/detectors/css/README.css', 'a').close()
2476 # use subdir in filename 2555 # use subdir in filename
2477 cl.serve_static_file("css/README.css") 2556 cl.serve_static_file("css/README.css")
2478 self.assertEqual(output[0][1], "text/css") 2557 self.assertEqual(output[0][2], "text/css")
2479 self.assertEqual(output[0][3], 2558 self.assertEqual(output[0][4],
2480 normpath("_test_cgi_form/detectors/css/README.css")) 2559 normpath("_test_cgi_form/detectors/css/README.css"))
2481 del output[0] # reset output buffer 2560 del output[0] # reset output buffer
2482 2561
2483 cl.Cache_Control['text/css'] = 'public, max-age=3600' 2562 cl.Cache_Control['text/css'] = 'public, max-age=3600'
2484 # use subdir in static files path 2563 # use subdir in static files path
2485 cl.instance.config['STATIC_FILES'] = 'detectors html/css' 2564 cl.instance.config['STATIC_FILES'] = 'detectors html/css'
2486 os.mkdir('_test_cgi_form/html/css') 2565 os.mkdir('_test_cgi_form/html/css')
2487 f = open('_test_cgi_form/html/css/README1.css', 'a').close() 2566 f = open('_test_cgi_form/html/css/README1.css', 'a').close()
2488 cl.serve_static_file("README1.css") 2567 cl.serve_static_file("README1.css")
2489 self.assertEqual(output[0][1], "text/css") 2568 self.assertEqual(output[0][2], "text/css")
2490 self.assertEqual(output[0][3], 2569 self.assertEqual(output[0][4],
2491 normpath("_test_cgi_form/html/css/README1.css")) 2570 normpath("_test_cgi_form/html/css/README1.css"))
2492 self.assertTrue( "Cache-Control" in cl.additional_headers ) 2571 self.assertTrue( "Cache-Control" in cl.additional_headers )
2493 self.assertEqual( cl.additional_headers, 2572 self.assertEqual( cl.additional_headers,
2494 {'Cache-Control': 'public, max-age=3600'} ) 2573 {'Cache-Control': 'public, max-age=3600'} )
2574 print(cl.additional_headers)
2495 del output[0] # reset output buffer 2575 del output[0] # reset output buffer
2496 2576
2497 cl.Cache_Control['README1.css'] = 'public, max-age=60' 2577 cl.Cache_Control['README1.css'] = 'public, max-age=60'
2498 cl.serve_static_file("README1.css") 2578 cl.serve_static_file("README1.css")
2499 self.assertEqual(output[0][1], "text/css") 2579 self.assertEqual(output[0][2], "text/css")
2500 self.assertEqual(output[0][3], 2580 self.assertEqual(output[0][4],
2501 normpath("_test_cgi_form/html/css/README1.css")) 2581 normpath("_test_cgi_form/html/css/README1.css"))
2502 self.assertTrue( "Cache-Control" in cl.additional_headers ) 2582 self.assertTrue( "Cache-Control" in cl.additional_headers )
2503 self.assertEqual( cl.additional_headers, 2583 self.assertEqual( cl.additional_headers,
2504 {'Cache-Control': 'public, max-age=60'} ) 2584 {'Cache-Control': 'public, max-age=60'} )
2505 del output[0] # reset output buffer 2585 del output[0] # reset output buffer

Roundup Issue Tracker: http://roundup-tracker.org/