1
0
Fork 0
forked from s3lph/matemat

More unit tests for the webserver.

This commit is contained in:
s3lph 2018-06-19 00:03:30 +02:00
parent 2f2f6b73e9
commit 9414b19dc2
2 changed files with 80 additions and 9 deletions

View file

@ -1,5 +1,5 @@
from typing import Any, Dict, Tuple from typing import Any, Callable, Dict, Tuple, Union
import unittest.mock import unittest.mock
from io import BytesIO from io import BytesIO
@ -8,6 +8,8 @@ from abc import ABC
from datetime import datetime from datetime import datetime
from http.server import HTTPServer from http.server import HTTPServer
from matemat.webserver.httpd import pagelet
class HttpResponse: class HttpResponse:
""" """
@ -27,6 +29,7 @@ class HttpResponse:
self.headers: Dict[str, str] = { self.headers: Dict[str, str] = {
'Content-Length': 0 'Content-Length': 0
} }
self.pagelet: str = None
# The response body. Only UTF-8 strings are supported # The response body. Only UTF-8 strings are supported
self.body: str = '' self.body: str = ''
# Parsing phase, one of 'begin', 'hdr', 'body' or 'done' # Parsing phase, one of 'begin', 'hdr', 'body' or 'done'
@ -34,6 +37,10 @@ class HttpResponse:
# Buffer for uncompleted lines # Buffer for uncompleted lines
self.buffer: bytes = bytes() self.buffer: bytes = bytes()
def __finalize(self):
self.parse_phase = 'done'
self.pagelet = self.headers['X-Test-Pagelet']
def parse(self, fragment: bytes) -> None: def parse(self, fragment: bytes) -> None:
""" """
Parse a new fragment of data. This function does nothing if the parsed HTTP response is already complete. Parse a new fragment of data. This function does nothing if the parsed HTTP response is already complete.
@ -47,7 +54,7 @@ class HttpResponse:
elif self.parse_phase == 'body': elif self.parse_phase == 'body':
self.body += fragment.decode('utf-8') self.body += fragment.decode('utf-8')
if len(self.body) >= int(self.headers['Content-Length']): if len(self.body) >= int(self.headers['Content-Length']):
self.parse_phase = 'done' self.__finalize()
return return
if b'\r\n' not in fragment: if b'\r\n' not in fragment:
# If the fragment does not contain a CR-LF, add it to the buffer, we only want to parse whole lines # If the fragment does not contain a CR-LF, add it to the buffer, we only want to parse whole lines
@ -82,7 +89,7 @@ class HttpResponse:
# if there is a remainder in the data packet, it is (part of) the body, add to body string # if there is a remainder in the data packet, it is (part of) the body, add to body string
self.body += line self.body += line
if len(self.body) >= int(self.headers['Content-Length']): if len(self.body) >= int(self.headers['Content-Length']):
self.parse_phase = 'done' self.__finalize()
class MockServer: class MockServer:
@ -144,6 +151,23 @@ class MockSocket(bytes):
return self.__packet return self.__packet
def test_pagelet(path: str):
def with_testing_headers(fun: Callable[[str, str, Dict[str, str], Dict[str, Any], Dict[str, str]],
Tuple[int, Union[bytes, str]]]):
@pagelet(path)
def testing_wrapper(method: str,
path: str,
args: Dict[str, str],
session_vars: Dict[str, Any],
headers: Dict[str, str]):
status, body = fun(method, path, args, session_vars, headers)
headers['X-Test-Pagelet'] = fun.__name__
return status, body
return testing_wrapper
return with_testing_headers
class AbstractHttpdTest(ABC, unittest.TestCase): class AbstractHttpdTest(ABC, unittest.TestCase):
""" """
An abstract test case that can be inherited by test case classes that want to test part of the webserver's core An abstract test case that can be inherited by test case classes that want to test part of the webserver's core

View file

@ -2,13 +2,18 @@
from typing import Any, Dict from typing import Any, Dict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep
from matemat.webserver.httpd import HttpHandler, pagelet from matemat.webserver.httpd import HttpHandler
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
@pagelet('/just/testing/sessions') @test_pagelet('/just/testing/sessions')
def test_pagelet(method: str, path: str, args: Dict[str, str], session_vars: Dict[str, Any], headers: Dict[str, str]): def session_test_pagelet(method: str,
path: str,
args: Dict[str, str],
session_vars: Dict[str, Any],
headers: Dict[str, str]):
session_vars['test'] = 'hello, world!' session_vars['test'] = 'hello, world!'
headers['Content-Type'] = 'text/plain' headers['Content-Type'] = 'text/plain'
return 200, 'session test' return 200, 'session test'
@ -21,7 +26,7 @@ class TestSession(AbstractHttpdTest):
def test_create_new_session(self): def test_create_new_session(self):
# Reference date to make sure the session expiry lies in the future # Reference date to make sure the session expiry lies in the future
refdate = datetime.utcnow() + timedelta(seconds=3500) refdate: datetime = datetime.utcnow() + timedelta(seconds=3500)
# Send a mock GET request for '/just/testing/sessions' # Send a mock GET request for '/just/testing/sessions'
self.client_sock.set_request(b'GET /just/testing/sessions HTTP/1.1\r\n\r\n') self.client_sock.set_request(b'GET /just/testing/sessions HTTP/1.1\r\n\r\n')
# Trigger request handling # Trigger request handling
@ -31,7 +36,7 @@ class TestSession(AbstractHttpdTest):
# Make sure a full HTTP response was parsed # Make sure a full HTTP response was parsed
self.assertEqual('done', packet.parse_phase) self.assertEqual('done', packet.parse_phase)
# Make sure the request was served by the test pagelet # Make sure the request was served by the test pagelet
self.assertEqual('session test', packet.body) self.assertEqual('session_test_pagelet', packet.pagelet)
self.assertEqual(200, packet.statuscode) self.assertEqual(200, packet.statuscode)
session_id: str = list(handler.server.session_vars.keys())[0] session_id: str = list(handler.server.session_vars.keys())[0]
@ -51,3 +56,45 @@ class TestSession(AbstractHttpdTest):
# Make sure the session exists on the server # Make sure the session exists on the server
self.assertIn('test', handler.session_vars) self.assertIn('test', handler.session_vars)
self.assertEqual('hello, world!', handler.session_vars['test']) self.assertEqual('hello, world!', handler.session_vars['test'])
def test_resume_session(self):
# Test session expiry date
refdate: datetime = datetime.utcnow() + timedelta(hours=1)
# Session ID for testing
session_id: str = 'testsessionid'
# Insert test session
self.server.session_vars[session_id] = refdate, {'test': 'bar'}
sleep(2)
# Send a mock GET request for '/just/testing/sessions' with a matemat session cookie
self.client_sock.set_request(
f'GET /just/testing/sessions HTTP/1.1\r\nCookie: matemat_session_id={session_id}\r\n'.encode('utf-8'))
# Trigger request handling
handler = HttpHandler(self.client_sock, ('::1', 45678), self.server)
# Fetch the parsed response
packet = self.client_sock.get_response()
# Make sure a full HTTP response was parsed
self.assertEqual('done', packet.parse_phase)
# Make sure the request was served by the test pagelet
self.assertEqual('session_test_pagelet', packet.pagelet)
self.assertEqual(200, packet.statuscode)
response_session_id: str = list(handler.server.session_vars.keys())[0]
# Make sure a cookie was set - assuming that only one was set
self.assertIn('Set-Cookie', packet.headers)
# Split into the cookie itself
cookie, expiry = packet.headers['Set-Cookie'].split(';')
cookie: str = cookie.strip()
expiry: str = expiry.strip()
# Make sure the 'matemat_session_id' cookie was set to the session ID string
self.assertEqual(f'matemat_session_id={response_session_id}', cookie)
# Make sure the session ID matches the one we sent along
self.assertEqual(session_id, response_session_id)
# Make sure the session timeout was postponed
self.assertTrue(expiry.startswith('expires='))
_, expdatestr = expiry.split('=', 1)
expdate = datetime.strptime(expdatestr, '%a, %d %b %Y %H:%M:%S GMT')
self.assertTrue(expdate > refdate)
# Make sure the session exists on the server
self.assertIn('test', handler.session_vars)
self.assertEqual('hello, world!', handler.session_vars['test'])