forked from s3lph/matemat
Further bottle porting
This commit is contained in:
parent
8e8f159150
commit
e5c3fad812
18 changed files with 66 additions and 387 deletions
2
doc
2
doc
|
@ -1 +1 @@
|
||||||
Subproject commit 0fcf4244f90d93cbc7be8e670aeaa30288d24ae3
|
Subproject commit 8d6d6b6fece9d0b2dedc11ad41d4b96554a8ea36
|
|
@ -3,6 +3,8 @@ from typing import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from threading import Event, Timer, Thread
|
from threading import Event, Timer, Thread
|
||||||
|
|
||||||
|
from matemat.webserver import Logger
|
||||||
|
|
||||||
_CRON_STATIC_EVENT: Event = Event()
|
_CRON_STATIC_EVENT: Event = Event()
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +69,7 @@ def cron(weeks: int = 0,
|
||||||
|
|
||||||
# This function is called once in the specified interval
|
# This function is called once in the specified interval
|
||||||
def cron():
|
def cron():
|
||||||
|
logger = Logger.instance()
|
||||||
# Reschedule the job
|
# Reschedule the job
|
||||||
t: Timer = _GlobalEventTimer(delta.total_seconds(), _CRON_STATIC_EVENT, cron)
|
t: Timer = _GlobalEventTimer(delta.total_seconds(), _CRON_STATIC_EVENT, cron)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
|
@ -247,7 +247,7 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
||||||
elif change == 'defaultimg':
|
elif change == 'defaultimg':
|
||||||
# Iterate the possible images to set
|
# Iterate the possible images to set
|
||||||
for category in 'users', 'products':
|
for category in 'users', 'products':
|
||||||
if category not in args:
|
if category not in files:
|
||||||
continue
|
continue
|
||||||
# Read the raw image data from the request
|
# Read the raw image data from the request
|
||||||
default: bytes = files[category].file.read()
|
default: bytes = files[category].file.read()
|
||||||
|
|
|
@ -18,7 +18,7 @@ def start() -> str:
|
||||||
"""
|
"""
|
||||||
Start a new session, or resume the session identified by the session cookie sent in the HTTP request.
|
Start a new session, or resume the session identified by the session cookie sent in the HTTP request.
|
||||||
|
|
||||||
:return: A tuple consisting of the session ID (a UUID string), and the session timeout date.
|
:return: The session ID.
|
||||||
"""
|
"""
|
||||||
# Reference date for session timeout
|
# Reference date for session timeout
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
|
|
|
@ -1,103 +1,18 @@
|
||||||
|
import threading
|
||||||
|
from http.client import HTTPResponse
|
||||||
from typing import Any, Callable, Dict, Tuple, Union
|
from typing import Any, Callable, Dict, Tuple, Union
|
||||||
|
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
from io import BytesIO
|
import urllib.request
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http.server import HTTPServer
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from matemat.webserver import pagelet, RequestArguments
|
import bottle
|
||||||
|
|
||||||
|
|
||||||
class HttpResponse:
|
|
||||||
"""
|
|
||||||
A really basic HTTP response container and parser class, just good enough for unit testing a HTTP server, if even.
|
|
||||||
|
|
||||||
DO NOT USE THIS OUTSIDE UNIT TESTING!
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
response = HttpResponse()
|
|
||||||
while response.parse_phase != 'done'
|
|
||||||
response.parse(<read from somewhere>)
|
|
||||||
print(response.statuscode)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
# The HTTP status code of the response
|
|
||||||
self.statuscode: int = 0
|
|
||||||
# HTTP headers set in the response
|
|
||||||
self.headers: Dict[str, str] = {
|
|
||||||
'Content-Length': 0
|
|
||||||
}
|
|
||||||
self.pagelet: str = None
|
|
||||||
# The response body
|
|
||||||
self.body: bytes = bytes()
|
|
||||||
# Parsing phase, one of 'begin', 'hdr', 'body' or 'done'
|
|
||||||
self.parse_phase = 'begin'
|
|
||||||
# Buffer for uncompleted lines
|
|
||||||
self.buffer: bytes = bytes()
|
|
||||||
|
|
||||||
def __finalize(self):
|
|
||||||
self.parse_phase = 'done'
|
|
||||||
self.pagelet = self.headers.get('X-Test-Pagelet', 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.
|
|
||||||
|
|
||||||
DO NOT USE THIS OUTSIDE UNIT TESTING!
|
|
||||||
|
|
||||||
:param fragment: The data fragment to parse.
|
|
||||||
"""
|
|
||||||
# response packet complete, nothing to do
|
|
||||||
if self.parse_phase == 'done':
|
|
||||||
return
|
|
||||||
# If in the body phase, simply decode and append to the body, while the body is not complete yet
|
|
||||||
elif self.parse_phase == 'body':
|
|
||||||
self.body += fragment
|
|
||||||
if len(self.body) >= int(self.headers['Content-Length']):
|
|
||||||
self.__finalize()
|
|
||||||
return
|
|
||||||
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
|
|
||||||
self.buffer = self.buffer + fragment
|
|
||||||
else:
|
|
||||||
if not fragment.endswith(b'\r\n'):
|
|
||||||
# Special treatment for no trailing CR-LF: Add remainder to buffer
|
|
||||||
head, tail = fragment.rsplit(b'\r\n', 1)
|
|
||||||
data: bytes = (self.buffer + head)
|
|
||||||
self.buffer = tail
|
|
||||||
else:
|
|
||||||
data: bytes = (self.buffer + fragment)
|
|
||||||
self.buffer = bytes()
|
|
||||||
# Iterate the lines that are ready to be parsed
|
|
||||||
for line in data.split(b'\r\n'):
|
|
||||||
# The 'begin' phase indicates that the parser is waiting for the HTTP status line
|
|
||||||
if self.parse_phase == 'begin':
|
|
||||||
if line.startswith(b'HTTP/'):
|
|
||||||
# Parse the statuscode and advance to header parsing
|
|
||||||
_, statuscode, _ = line.decode('utf-8').split(' ', 2)
|
|
||||||
self.statuscode = int(statuscode)
|
|
||||||
self.parse_phase = 'hdr'
|
|
||||||
elif self.parse_phase == 'hdr':
|
|
||||||
# Parse a header line and add it to the header dict
|
|
||||||
if len(line) > 0:
|
|
||||||
k, v = line.decode('utf-8').split(':', 1)
|
|
||||||
self.headers[k.strip()] = v.strip()
|
|
||||||
else:
|
|
||||||
# Empty line separates header from body
|
|
||||||
self.parse_phase = 'body'
|
|
||||||
elif self.parse_phase == 'body':
|
|
||||||
# if there is a remainder in the data packet, it is (part of) the body, add to body string
|
|
||||||
self.body += line
|
|
||||||
if len(self.body) >= int(self.headers['Content-Length']):
|
|
||||||
self.__finalize()
|
|
||||||
|
|
||||||
|
|
||||||
class MockServer:
|
class MockServer:
|
||||||
|
@ -129,76 +44,7 @@ class MockServer:
|
||||||
# sh: logging.StreamHandler = logging.StreamHandler()
|
# sh: logging.StreamHandler = logging.StreamHandler()
|
||||||
# sh.setFormatter(logging.Formatter('%(asctime)s %(name)s [%(levelname)s]: %(message)s'))
|
# sh.setFormatter(logging.Formatter('%(asctime)s %(name)s [%(levelname)s]: %(message)s'))
|
||||||
# self.logger.addHandler(sh)
|
# self.logger.addHandler(sh)
|
||||||
|
bottle.run(host='::1', port='8888')
|
||||||
|
|
||||||
class MockSocket(bytes):
|
|
||||||
"""
|
|
||||||
A mock implementation of a socket.socket for http.server.BaseHTTPRequestHandler.
|
|
||||||
|
|
||||||
The bytes inheritance is due to a broken type annotation in BaseHTTPRequestHandler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
# The request string
|
|
||||||
self.__request = bytes()
|
|
||||||
# The parsed response
|
|
||||||
self.__packet = HttpResponse()
|
|
||||||
|
|
||||||
def set_request(self, request: bytes) -> None:
|
|
||||||
"""
|
|
||||||
Sets the HTTP request to send to the server.
|
|
||||||
|
|
||||||
:param request: The request
|
|
||||||
"""
|
|
||||||
self.__request: bytes = request
|
|
||||||
|
|
||||||
def makefile(self, mode: str, size: int) -> BytesIO:
|
|
||||||
"""
|
|
||||||
Required by http.server.HTTPServer.
|
|
||||||
|
|
||||||
:return: A dummy buffer IO object instead of a network socket file handle.
|
|
||||||
"""
|
|
||||||
return BytesIO(self.__request)
|
|
||||||
|
|
||||||
def sendall(self, b: bytes) -> None:
|
|
||||||
"""
|
|
||||||
Required by http.server.HTTPServer.
|
|
||||||
|
|
||||||
:param b: The data to send to the client. Will be parsed directly instead.
|
|
||||||
"""
|
|
||||||
self.__packet.parse(b)
|
|
||||||
|
|
||||||
def get_response(self) -> HttpResponse:
|
|
||||||
"""
|
|
||||||
Fetches the parsed HTTP response generated by the server.
|
|
||||||
|
|
||||||
:return: The response object.
|
|
||||||
"""
|
|
||||||
return self.__packet
|
|
||||||
|
|
||||||
|
|
||||||
def test_pagelet(path: str):
|
|
||||||
|
|
||||||
def with_testing_headers(fun: Callable[[str,
|
|
||||||
str,
|
|
||||||
RequestArguments,
|
|
||||||
Dict[str, Any],
|
|
||||||
Dict[str, str],
|
|
||||||
Dict[str, str]],
|
|
||||||
Union[bytes, str, Tuple[int, str]]]):
|
|
||||||
@pagelet(path)
|
|
||||||
def testing_wrapper(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]):
|
|
||||||
headers['X-Test-Pagelet'] = fun.__name__
|
|
||||||
result = fun(method, path, args, session_vars, headers, pagelet_variables)
|
|
||||||
return result
|
|
||||||
return testing_wrapper
|
|
||||||
return with_testing_headers
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractHttpdTest(ABC, unittest.TestCase):
|
class AbstractHttpdTest(ABC, unittest.TestCase):
|
||||||
|
@ -215,8 +61,13 @@ class AbstractHttpdTest(ABC, unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.tempdir: TemporaryDirectory = TemporaryDirectory(prefix='matemat.', dir='/tmp/')
|
self.tempdir: TemporaryDirectory = TemporaryDirectory(prefix='matemat.', dir='/tmp/')
|
||||||
self.server: HTTPServer = MockServer(webroot=self.tempdir.name)
|
self.server = threading.Thread(target=bottle.run, kwargs={'host': '::1', 'port': 8888, 'debug': True})
|
||||||
self.client_sock: MockSocket = MockSocket()
|
self.server.start()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
self.server._stop()
|
||||||
self.tempdir.cleanup()
|
self.tempdir.cleanup()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def request(query) -> HTTPResponse:
|
||||||
|
return urllib.request.urlopen(f'http://[::1]:8888{query}')
|
||||||
|
|
|
@ -8,7 +8,7 @@ from io import StringIO
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from matemat.webserver import parse_config_file
|
from matemat.webserver.config import parse_config_file
|
||||||
|
|
||||||
_EMPTY_CONFIG = ''
|
_EMPTY_CONFIG = ''
|
||||||
|
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
from typing import Any, Dict, Union
|
|
||||||
|
|
||||||
from matemat.exceptions import HttpException
|
|
||||||
from matemat.webserver import HttpHandler, RequestArguments, PageletResponse
|
|
||||||
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
|
|
||||||
|
|
||||||
|
|
||||||
@test_pagelet('/just/testing/http_exception')
|
|
||||||
def test_pagelet_http_exception(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
|
|
||||||
raise HttpException(int(str(args.exc)), 'Test Exception')
|
|
||||||
|
|
||||||
|
|
||||||
@test_pagelet('/just/testing/value_error')
|
|
||||||
def test_pagelet_value_error(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
|
|
||||||
raise ValueError('test')
|
|
||||||
|
|
||||||
|
|
||||||
@test_pagelet('/just/testing/permission_error')
|
|
||||||
def test_pagelet_permission_error(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
|
|
||||||
raise PermissionError('test')
|
|
||||||
|
|
||||||
|
|
||||||
@test_pagelet('/just/testing/other_error')
|
|
||||||
def test_pagelet_other_error(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]) -> Union[bytes, str, PageletResponse]:
|
|
||||||
raise TypeError('test')
|
|
||||||
|
|
||||||
|
|
||||||
class TestHttpd(AbstractHttpdTest):
|
|
||||||
|
|
||||||
def test_httpd_get_illegal_path(self):
|
|
||||||
self.client_sock.set_request(b'GET /foo?bar?baz HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(400, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_post_illegal_path(self):
|
|
||||||
self.client_sock.set_request(b'POST /foo?bar?baz HTTP/1.1\r\n'
|
|
||||||
b'Content-Length: 0\r\n'
|
|
||||||
b'Content-Type: application/x-www-form-urlencoded\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(400, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_post_illegal_header(self):
|
|
||||||
self.client_sock.set_request(b'POST /foo?bar=baz HTTP/1.1\r\n'
|
|
||||||
b'Content-Length: 0\r\n'
|
|
||||||
b'Content-Type: application/octet-stream\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(400, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_post_request_too_big(self):
|
|
||||||
self.client_sock.set_request(b'POST /foo?bar=baz HTTP/1.1\r\n'
|
|
||||||
b'Content-Length: 1000001\r\n'
|
|
||||||
b'Content-Type: application/octet-stream\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(413, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_exception_http_400(self):
|
|
||||||
self.client_sock.set_request(b'GET /just/testing/http_exception?exc=400 HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(400, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_exception_http_500(self):
|
|
||||||
self.client_sock.set_request(b'GET /just/testing/http_exception?exc=500 HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(500, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_exception_value_error(self):
|
|
||||||
self.client_sock.set_request(b'GET /just/testing/value_error HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(400, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_exception_permission_error(self):
|
|
||||||
self.client_sock.set_request(b'GET /just/testing/permission_error HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(403, packet.statuscode)
|
|
||||||
|
|
||||||
def test_httpd_exception_other_error(self):
|
|
||||||
self.client_sock.set_request(b'GET /just/testing/other_error HTTP/1.1\r\n\r\n')
|
|
||||||
HttpHandler(self.client_sock, ('::1', 45678), self.server)
|
|
||||||
packet = self.client_sock.get_response()
|
|
||||||
self.assertEqual(500, packet.statuscode)
|
|
|
@ -1,68 +0,0 @@
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import http.client
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from matemat.webserver import MatematWebserver, RequestArguments, pagelet_init, pagelet
|
|
||||||
|
|
||||||
|
|
||||||
@pagelet('/just/testing/init')
|
|
||||||
def init_test_pagelet(method: str,
|
|
||||||
path: str,
|
|
||||||
args: RequestArguments,
|
|
||||||
session_vars: Dict[str, Any],
|
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]):
|
|
||||||
return pagelet_variables['Unit-Test']
|
|
||||||
|
|
||||||
|
|
||||||
_INIT_FAIL = False
|
|
||||||
|
|
||||||
|
|
||||||
@pagelet_init
|
|
||||||
def init(config: Dict[str, str],
|
|
||||||
logger: logging.Logger):
|
|
||||||
if _INIT_FAIL:
|
|
||||||
raise ValueError('This error should be raised!')
|
|
||||||
config['Unit-Test'] = 'Pagelet Init Test'
|
|
||||||
|
|
||||||
|
|
||||||
class TestPageletInitialization(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.srv = MatematWebserver('::1', 0, '/nonexistent', '/nonexistent', {}, {},
|
|
||||||
logging.NOTSET, logging.NullHandler())
|
|
||||||
self.srv_port = int(self.srv._httpd.socket.getsockname()[1])
|
|
||||||
self.timer = threading.Timer(5.0, self.srv._httpd.shutdown)
|
|
||||||
self.timer.start()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.timer.cancel()
|
|
||||||
if self.srv is not None:
|
|
||||||
self.srv._httpd.socket.close()
|
|
||||||
global _INIT_FAIL
|
|
||||||
_INIT_FAIL = False
|
|
||||||
|
|
||||||
def test_pagelet_init_ok(self):
|
|
||||||
"""
|
|
||||||
Test successful pagelet initialization
|
|
||||||
"""
|
|
||||||
thread = threading.Thread(target=self.srv.start)
|
|
||||||
thread.start()
|
|
||||||
con = http.client.HTTPConnection(f'[::1]:{self.srv_port}')
|
|
||||||
con.request('GET', '/just/testing/init')
|
|
||||||
response = con.getresponse().read()
|
|
||||||
self.srv._httpd.shutdown()
|
|
||||||
self.assertEqual(b'Pagelet Init Test', response)
|
|
||||||
|
|
||||||
def test_pagelet_init_fail(self):
|
|
||||||
"""
|
|
||||||
Test unsuccessful pagelet initialization
|
|
||||||
"""
|
|
||||||
global _INIT_FAIL
|
|
||||||
_INIT_FAIL = True
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
self.srv.start()
|
|
|
@ -1,26 +1,22 @@
|
||||||
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from matemat.webserver import HttpHandler, RequestArguments
|
from bottle import route, run
|
||||||
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest, test_pagelet
|
|
||||||
|
from matemat.webserver import session
|
||||||
|
from matemat.webserver.test.abstract_httpd_test import AbstractHttpdTest
|
||||||
|
|
||||||
|
|
||||||
@test_pagelet('/just/testing/sessions')
|
@route('/just/testing/sessions')
|
||||||
def session_test_pagelet(method: str,
|
def session_test_pagelet():
|
||||||
path: str,
|
s = session.start()
|
||||||
args: RequestArguments,
|
session.put(s, 'test', 'hello, world!')
|
||||||
session_vars: Dict[str, Any],
|
return f'{s}'
|
||||||
headers: Dict[str, str],
|
|
||||||
pagelet_variables: Dict[str, str]):
|
|
||||||
session_vars['test'] = 'hello, world!'
|
|
||||||
headers['Content-Type'] = 'text/plain'
|
|
||||||
return 'session test'
|
|
||||||
|
|
||||||
|
|
||||||
class TestSession(AbstractHttpdTest):
|
class TestSession(AbstractHttpdTest):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Test session handling of the Matemat webserver.
|
Test session handling of the Matemat webserver.
|
||||||
"""
|
"""
|
||||||
|
@ -29,18 +25,12 @@ class TestSession(AbstractHttpdTest):
|
||||||
# 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 = 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')
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
session_id: str = list(handler.server.session_vars.keys())[0]
|
packet = self.request('/just/testing/sessions')
|
||||||
|
# Make sure the request was served by the test pagelet
|
||||||
|
self.assertEqual(200, packet.code)
|
||||||
|
|
||||||
|
session_id: str = packet.read().decode()
|
||||||
# Make sure a cookie was set - assuming that only one was set
|
# Make sure a cookie was set - assuming that only one was set
|
||||||
self.assertIn('Set-Cookie', packet.headers)
|
self.assertIn('Set-Cookie', packet.headers)
|
||||||
# Split into the cookie itself
|
# Split into the cookie itself
|
||||||
|
@ -54,9 +44,6 @@ class TestSession(AbstractHttpdTest):
|
||||||
_, expdatestr = expiry.split('=', 1)
|
_, expdatestr = expiry.split('=', 1)
|
||||||
expdate = datetime.strptime(expdatestr, '%a, %d %b %Y %H:%M:%S GMT')
|
expdate = datetime.strptime(expdatestr, '%a, %d %b %Y %H:%M:%S GMT')
|
||||||
self.assertTrue(expdate > refdate)
|
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'])
|
|
||||||
|
|
||||||
def test_resume_session(self):
|
def test_resume_session(self):
|
||||||
# Test session expiry date
|
# Test session expiry date
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.thumblist-item {
|
.thumblist-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 10px;
|
margin: 5px;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumblist-item .imgcontainer {
|
.thumblist-item .imgcontainer {
|
||||||
width: 150px;
|
width: 100px;
|
||||||
height: 150px;
|
height: 100px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumblist-item .imgcontainer img {
|
.thumblist-item .imgcontainer img {
|
||||||
max-width: 150px;
|
max-width: 100px;
|
||||||
max-height: 150px;
|
max-height: 100px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -30,6 +30,15 @@
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumblist-stock {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<h2>Avatar</h2>
|
<h2>Avatar</h2>
|
||||||
|
|
||||||
<form id="admin-avatar-form" method="post" action="/admin?change=avatar" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form id="admin-avatar-form" method="post" action="/admin?change=avatar" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<img src="/upload/thumbnails/users/{{ authuser.id }}.png" alt="Avatar of {{ authuser.name }}" /><br/>
|
<img src="/static/upload/thumbnails/users/{{ authuser.id }}.png" alt="Avatar of {{ authuser.name }}" /><br/>
|
||||||
|
|
||||||
<label for="admin-avatar-avatar">Upload new file: </label>
|
<label for="admin-avatar-avatar">Upload new file: </label>
|
||||||
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
||||||
|
|
|
@ -94,12 +94,12 @@
|
||||||
|
|
||||||
<form id="admin-default-images-form" method="post" action="/admin?adminchange=defaultimg" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form id="admin-default-images-form" method="post" action="/admin?adminchange=defaultimg" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<label for="admin-default-images-user">
|
<label for="admin-default-images-user">
|
||||||
<img src="/upload/thumbnails/users/default.png" alt="Default user avatar" />
|
<img src="/static/upload/thumbnails/users/default.png" alt="Default user avatar" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="admin-default-images-user" type="file" name="users" accept="image/*" /><br/>
|
<input id="admin-default-images-user" type="file" name="users" accept="image/*" /><br/>
|
||||||
|
|
||||||
<label for="admin-default-images-product">
|
<label for="admin-default-images-product">
|
||||||
<img src="/upload/thumbnails/products/default.png" alt="Default product avatar" />
|
<img src="/static/upload/thumbnails/products/default.png" alt="Default product avatar" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="admin-default-images-product" type="file" name="products" accept="image/*" /><br/>
|
<input id="admin-default-images-product" type="file" name="products" accept="image/*" /><br/>
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li> {{ setupname|safe }}
|
<li> {{ setupname|safe }}
|
||||||
<li> Matemat {{ __version__ }}
|
<li> Matemat {{ __version__ }}
|
||||||
<li> © 2018 s3lph
|
|
||||||
<li> MIT License
|
<li> MIT License
|
||||||
{# This used to be a link to the GitLab repo. However, users of the testing environment always clicked
|
{# This used to be a link to the GitLab repo. However, users of the testing environment always clicked
|
||||||
that link and couldn't come back, because the UI was running in touch-only kiosk mode. #}
|
that link and couldn't come back, because the UI was running in touch-only kiosk mode. #}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
||||||
|
|
||||||
<label for="modproduct-image">
|
<label for="modproduct-image">
|
||||||
<img height="150" src="/upload/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" />
|
<img height="150" src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
|
<input id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<input id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/>
|
<input id="moduser-account-balance-reason" type="text" name="reason" placeholder="Shows up on receipt" /><br/>
|
||||||
|
|
||||||
<label for="moduser-account-avatar">
|
<label for="moduser-account-avatar">
|
||||||
<img src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
<img src="/static/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
||||||
</label><br/>
|
</label><br/>
|
||||||
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/*" /><br/>
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,13 @@
|
||||||
Your balance: {{ authuser.balance|chf }}
|
Your balance: {{ authuser.balance|chf }}
|
||||||
<br/>
|
<br/>
|
||||||
{# Links to deposit two common amounts of cash. TODO: Will be replaced by a nicer UI later (#20) #}
|
{# Links to deposit two common amounts of cash. TODO: Will be replaced by a nicer UI later (#20) #}
|
||||||
<a href="/deposit?n=100">Deposit CHF 1</a><br/>
|
<div class="thumblist-item">
|
||||||
<a href="/deposit?n=1000">Deposit CHF 10</a><br/>
|
<a href="/deposit?n=100">Deposit CHF 1</a>
|
||||||
|
</div>
|
||||||
|
<div class="thumblist-item">
|
||||||
|
<a href="/deposit?n=1000">Deposit CHF 10</a>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
||||||
|
@ -26,9 +31,10 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ product.price_non_member|chf }}
|
{{ product.price_non_member|chf }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
; Stock: {{ product.stock }}</span><br/>
|
</span><br/>
|
||||||
<div class="imgcontainer">
|
<div class="imgcontainer">
|
||||||
<img src="/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
|
<img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
|
||||||
|
<span class="thumblist-stock">{{ product.stock }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
width: 600px;
|
width: 400px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="/touchkey?uid={{ user.id }}&username={{ user.name }}">
|
<a href="/touchkey?uid={{ user.id }}&username={{ user.name }}">
|
||||||
<span class="thumblist-title">{{ user.name }}</span><br/>
|
<span class="thumblist-title">{{ user.name }}</span><br/>
|
||||||
<div class="imgcontainer">
|
<div class="imgcontainer">
|
||||||
<img src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}"/>
|
<img src="/static/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}"/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue