Mercurial > p > roundup > code
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 |
