529 lines
20 KiB
Python
529 lines
20 KiB
Python
|
|
from typing import Dict, List, Set, Tuple
|
|
|
|
import unittest
|
|
import urllib.parse
|
|
|
|
from matemat.webserver import RequestArgument, RequestArguments
|
|
# noinspection PyProtectedMember
|
|
from matemat.webserver.requestargs import _View
|
|
|
|
|
|
class TestRequestArguments(unittest.TestCase):
|
|
"""
|
|
Test cases for the RequestArgument class.
|
|
"""
|
|
|
|
def test_create_default(self):
|
|
"""
|
|
Test creation of an empty RequestArgument
|
|
"""
|
|
ra = RequestArgument('foo')
|
|
# Name must be set to 1st argument
|
|
self.assertEqual('foo', ra.name)
|
|
# Must be a 0-length array
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
self.assertTrue(ra.is_array)
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_create_str_scalar(self):
|
|
"""
|
|
Test creation of a scalar RequestArgument with string value.
|
|
"""
|
|
ra = RequestArgument('foo', ('text/plain', 'bar'))
|
|
# Name must be set to 1st argument
|
|
self.assertEqual('foo', ra.name)
|
|
# Must be a scalar, length 1
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
self.assertFalse(ra.is_array)
|
|
# Scalar value must be representable both as str and bytes
|
|
self.assertEqual('bar', ra.get_str())
|
|
self.assertEqual(b'bar', ra.get_bytes())
|
|
# Content-Type must be set correctly
|
|
self.assertEqual('text/plain', ra.get_content_type())
|
|
# Using 0 indices must yield the same results
|
|
self.assertEqual('bar', ra.get_str(0))
|
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
|
self.assertEqual('text/plain', ra.get_content_type(0))
|
|
# Using other indices must result in an error
|
|
with self.assertRaises(IndexError):
|
|
ra.get_str(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_bytes(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_content_type(1)
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_create_str_scalar_array(self):
|
|
"""
|
|
Test creation of a scalar RequestArgument with string value, passing an array instead of a single tuple.
|
|
"""
|
|
ra = RequestArgument('foo', [('text/plain', 'bar')])
|
|
# Name must be set to 1st argument
|
|
self.assertEqual('foo', ra.name)
|
|
# Must be a scalar, length 1
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
self.assertFalse(ra.is_array)
|
|
# Scalar value must be representable both as str and bytes
|
|
self.assertEqual('bar', ra.get_str())
|
|
self.assertEqual(b'bar', ra.get_bytes())
|
|
# Content-Type must be set correctly
|
|
self.assertEqual('text/plain', ra.get_content_type())
|
|
# Using 0 indices must yield the same results
|
|
self.assertEqual('bar', ra.get_str(0))
|
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
|
self.assertEqual('text/plain', ra.get_content_type(0))
|
|
# Using other indices must result in an error
|
|
with self.assertRaises(IndexError):
|
|
ra.get_str(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_bytes(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_content_type(1)
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_create_bytes_scalar(self):
|
|
"""
|
|
Test creation of a scalar RequestArgument with bytes value.
|
|
"""
|
|
ra = RequestArgument('foo', ('application/octet-stream', b'\x00\x80\xff\xfe'))
|
|
# Name must be set to 1st argument
|
|
self.assertEqual('foo', ra.name)
|
|
# Must be a scalar, length 1
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
self.assertFalse(ra.is_array)
|
|
# Conversion to UTF-8 string must fail; bytes representation must work
|
|
with self.assertRaises(UnicodeDecodeError):
|
|
ra.get_str()
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
|
|
# Content-Type must be set correctly
|
|
self.assertEqual('application/octet-stream', ra.get_content_type())
|
|
# Using 0 indices must yield the same results
|
|
with self.assertRaises(UnicodeDecodeError):
|
|
ra.get_str(0)
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(0))
|
|
self.assertEqual('application/octet-stream', ra.get_content_type(0))
|
|
# Using other indices must result in an error
|
|
with self.assertRaises(IndexError):
|
|
ra.get_str(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_bytes(1)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_content_type(1)
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_create_array(self):
|
|
"""
|
|
Test creation of an array RequestArgument with mixed str and bytes initial value.
|
|
"""
|
|
ra = RequestArgument('foo', [
|
|
('text/plain', 'bar'),
|
|
('application/octet-stream', b'\x00\x80\xff\xfe')
|
|
])
|
|
# Name must be set to 1st argument
|
|
self.assertEqual('foo', ra.name)
|
|
# Must be an array, length 2
|
|
self.assertEqual(2, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
self.assertTrue(ra.is_array)
|
|
# Retrieving values without an index must yield the first element
|
|
self.assertEqual('bar', ra.get_str())
|
|
self.assertEqual(b'bar', ra.get_bytes())
|
|
self.assertEqual('text/plain', ra.get_content_type())
|
|
# The first value must be representable both as str and bytes, and have ctype text/plain
|
|
self.assertEqual('bar', ra.get_str(0))
|
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
|
self.assertEqual('text/plain', ra.get_content_type(0))
|
|
# Conversion of the second value to UTF-8 string must fail; bytes representation must work
|
|
with self.assertRaises(UnicodeDecodeError):
|
|
ra.get_str(1)
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
|
|
# The second value's ctype must be correct
|
|
self.assertEqual('application/octet-stream', ra.get_content_type(1))
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_append_empty_str(self):
|
|
"""
|
|
Test appending a str value to an empty RequestArgument.
|
|
"""
|
|
# Initialize the empty RequestArgument
|
|
ra = RequestArgument('foo')
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
|
|
# Append a string value
|
|
ra.append('text/plain', 'bar')
|
|
# New length must be 1, empty array must be converted to scalar
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
# Retrieval of the new value must work both in str and bytes representation
|
|
self.assertEqual('bar', ra.get_str())
|
|
self.assertEqual(b'bar', ra.get_bytes())
|
|
# Content type of the new value must be correct
|
|
self.assertEqual('text/plain', ra.get_content_type())
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_append_empty_bytes(self):
|
|
"""
|
|
Test appending a bytes value to an empty RequestArgument.
|
|
"""
|
|
# Initialize the empty RequestArgument
|
|
ra = RequestArgument('foo')
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
|
|
# Append a bytes value
|
|
ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
|
|
# New length must be 1, empty array must be converted to scalar
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
# Conversion of the new value to UTF-8 string must fail; bytes representation must work
|
|
with self.assertRaises(UnicodeDecodeError):
|
|
ra.get_str()
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes())
|
|
# Content type of the new value must be correct
|
|
self.assertEqual('application/octet-stream', ra.get_content_type())
|
|
# Must not be a view
|
|
self.assertFalse(ra.is_view)
|
|
|
|
def test_append_multiple(self):
|
|
"""
|
|
Test appending multiple values to an empty RequestArgument.
|
|
"""
|
|
# Initialize the empty RequestArgument
|
|
ra = RequestArgument('foo')
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
|
|
# Append a first value
|
|
ra.append('text/plain', 'bar')
|
|
# New length must be 1, empty array must be converted to scalar
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
self.assertEqual(b'bar', ra.get_bytes())
|
|
|
|
# Append a second value
|
|
ra.append('application/octet-stream', b'\x00\x80\xff\xfe')
|
|
# New length must be 2, scalar must be converted to array
|
|
self.assertEqual(2, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
|
|
|
|
# Append a third value
|
|
ra.append('text/plain', 'Hello, World!')
|
|
# New length must be 3, array must remain array
|
|
self.assertEqual(3, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
self.assertEqual(b'bar', ra.get_bytes(0))
|
|
self.assertEqual(b'\x00\x80\xff\xfe', ra.get_bytes(1))
|
|
self.assertEqual(b'Hello, World!', ra.get_bytes(2))
|
|
|
|
def test_clear_empty(self):
|
|
"""
|
|
Test clearing an empty RequestArgument.
|
|
"""
|
|
# Initialize the empty RequestArgument
|
|
ra = RequestArgument('foo')
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
ra.clear()
|
|
# Clearing an empty RequestArgument shouldn't have any effect
|
|
self.assertEqual('foo', ra.name)
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
|
|
def test_clear_scalar(self):
|
|
"""
|
|
Test clearing a scalar RequestArgument.
|
|
"""
|
|
# Initialize the scalar RequestArgument
|
|
ra = RequestArgument('foo', ('text/plain', 'bar'))
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue(ra.is_scalar)
|
|
ra.clear()
|
|
# Clearing a scalar RequestArgument should reduce its size to 0
|
|
self.assertEqual('foo', ra.name)
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_str()
|
|
|
|
def test_clear_array(self):
|
|
"""
|
|
Test clearing an array RequestArgument.
|
|
"""
|
|
# Initialize the array RequestArgument
|
|
ra = RequestArgument('foo', [
|
|
('text/plain', 'bar'),
|
|
('application/octet-stream', b'\x00\x80\xff\xfe'),
|
|
('text/plain', 'baz'),
|
|
])
|
|
self.assertEqual(3, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
ra.clear()
|
|
# Clearing an array RequestArgument should reduce its size to 0
|
|
self.assertEqual('foo', ra.name)
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse(ra.is_scalar)
|
|
with self.assertRaises(IndexError):
|
|
ra.get_str()
|
|
|
|
def test_iterate_empty(self):
|
|
"""
|
|
Test iterating an empty RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo')
|
|
self.assertEqual(0, len(ra))
|
|
# No value must be yielded from iterating an empty instance
|
|
for _ in ra:
|
|
self.fail()
|
|
|
|
def test_iterate_scalar(self):
|
|
"""
|
|
Test iterating a scalar RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', ('text/plain', 'bar'))
|
|
self.assertTrue(ra.is_scalar)
|
|
# Counter for the number of iterations
|
|
count: int = 0
|
|
for it in ra:
|
|
# Make sure the yielded value is a scalar view and has the same name as the original instance
|
|
self.assertIsInstance(it, _View)
|
|
self.assertTrue(it.is_view)
|
|
self.assertEqual('foo', it.name)
|
|
self.assertTrue(it.is_scalar)
|
|
count += 1
|
|
# Only one value must be yielded from iterating a scalar instance
|
|
self.assertEqual(1, count)
|
|
|
|
def test_iterate_array(self):
|
|
"""
|
|
Test iterating an array RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', [('text/plain', 'bar'), ('abc', b'def'), ('xyz', '1337')])
|
|
self.assertFalse(ra.is_scalar)
|
|
# Container to put the iterated ctypes into
|
|
items: List[str] = list()
|
|
for it in ra:
|
|
# Make sure the yielded values are scalar views and have the same name as the original instance
|
|
self.assertIsInstance(it, _View)
|
|
self.assertTrue(it.is_view)
|
|
self.assertTrue(it.is_scalar)
|
|
# Collect the value's ctype
|
|
items.append(it.get_content_type())
|
|
# Compare collected ctypes with expected result
|
|
self.assertEqual(['text/plain', 'abc', 'xyz'], items)
|
|
|
|
def test_slice(self):
|
|
"""
|
|
Test slicing an array RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j'), ('k', 'l')])
|
|
# Create the sliced view
|
|
sliced = ra[1:4:2]
|
|
# Make sure the sliced value is a view
|
|
self.assertIsInstance(sliced, _View)
|
|
self.assertTrue(sliced.is_view)
|
|
# Make sure the slice has the same name
|
|
self.assertEqual('foo', sliced.name)
|
|
# Make sure the slice has the expected shape (array of the 2nd and 4th scalar in the original)
|
|
self.assertTrue(sliced.is_array)
|
|
self.assertEqual(2, len(sliced))
|
|
self.assertEqual('d', sliced.get_str(0))
|
|
self.assertEqual('h', sliced.get_str(1))
|
|
|
|
def test_iterate_sliced(self):
|
|
"""
|
|
Test iterating a sliced array RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j'), ('k', 'l')])
|
|
# Container to put the iterated ctypes into
|
|
items: List[str] = list()
|
|
# Iterate the sliced view
|
|
for it in ra[1:4:2]:
|
|
# Make sure the yielded values are scalar views and have the same name as the original instance
|
|
self.assertIsInstance(it, _View)
|
|
self.assertTrue(it.is_view)
|
|
self.assertEqual('foo', it.name)
|
|
self.assertTrue(it.is_scalar)
|
|
items.append(it.get_content_type())
|
|
# Make sure the expected values are collected (array of the 2nd and 4th scalar in the original)
|
|
self.assertEqual(['c', 'g'], items)
|
|
|
|
def test_index_scalar(self):
|
|
"""
|
|
Test indexing of a scalar RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', ('bar', 'baz'))
|
|
# Index the scalar RequestArgument instance, obtaining an immutable view
|
|
it = ra[0]
|
|
# Make sure the value is a scalar view with the same properties as the original instance
|
|
self.assertIsInstance(it, _View)
|
|
self.assertTrue(it.is_scalar)
|
|
self.assertEqual('foo', it.name)
|
|
self.assertEqual('bar', it.get_content_type())
|
|
self.assertEqual('baz', it.get_str())
|
|
# Make sure other indices don't work
|
|
with self.assertRaises(IndexError):
|
|
_ = ra[1]
|
|
|
|
def test_index_array(self):
|
|
"""
|
|
Test indexing of an array RequestArgument.
|
|
"""
|
|
ra = RequestArgument('foo', [('a', 'b'), ('c', 'd')])
|
|
# Index the array RequestArgument instance, obtaining an immutable view
|
|
it = ra[1]
|
|
# Make sure the value is a scalar view with the same properties as the value in the original instance
|
|
self.assertIsInstance(it, _View)
|
|
self.assertEqual('foo', it.name)
|
|
self.assertEqual('c', it.get_content_type())
|
|
self.assertEqual('d', it.get_str())
|
|
|
|
def test_view_immutable(self):
|
|
"""
|
|
Test immutability of views.
|
|
"""
|
|
ra = RequestArgument('foo', ('bar', 'baz'))
|
|
# Index the scalar RequestArgument instance, obtaining an immutable view
|
|
it = ra[0]
|
|
# Make sure the returned value is a view
|
|
self.assertIsInstance(it, _View)
|
|
# Make sure the returned value is immutable
|
|
with self.assertRaises(TypeError):
|
|
it.append('foo', 'bar')
|
|
with self.assertRaises(TypeError):
|
|
it.clear()
|
|
|
|
def test_str_shorthand(self):
|
|
"""
|
|
Test the shorthand for get_str(0).
|
|
"""
|
|
ra = RequestArgument('foo', ('bar', 'baz'))
|
|
self.assertEqual('baz', str(ra))
|
|
|
|
def test_bytes_shorthand(self):
|
|
"""
|
|
Test the shorthand for get_bytes(0).
|
|
"""
|
|
ra = RequestArgument('foo', ('bar', b'\x00\x80\xff\xfe'))
|
|
self.assertEqual(b'\x00\x80\xff\xfe', bytes(ra))
|
|
|
|
# noinspection PyTypeChecker
|
|
def test_insert_garbage(self):
|
|
"""
|
|
Test proper handling with non-int indices and non-str/non-bytes data
|
|
:return:
|
|
"""
|
|
ra = RequestArgument('foo', 42)
|
|
with self.assertRaises(TypeError):
|
|
str(ra)
|
|
ra = RequestArgument('foo', (None, 42))
|
|
with self.assertRaises(TypeError):
|
|
str(ra)
|
|
with self.assertRaises(TypeError):
|
|
bytes(ra)
|
|
with self.assertRaises(TypeError):
|
|
ra.get_content_type()
|
|
with self.assertRaises(TypeError):
|
|
ra.get_str('foo')
|
|
with self.assertRaises(TypeError):
|
|
ra.get_bytes('foo')
|
|
with self.assertRaises(TypeError):
|
|
ra.get_content_type('foo')
|
|
|
|
def test_requestarguments_index(self):
|
|
"""
|
|
Make sure indexing a RequestArguments instance creates a new entry on the fly.
|
|
"""
|
|
ra = RequestArguments()
|
|
self.assertEqual(0, len(ra))
|
|
self.assertFalse('foo' in ra)
|
|
# Create new entry
|
|
_ = ra['foo']
|
|
self.assertEqual(1, len(ra))
|
|
self.assertTrue('foo' in ra)
|
|
# Already exists, no new entry created
|
|
_ = ra['foo']
|
|
self.assertEqual(1, len(ra))
|
|
# Entry must be empty and mutable, and have the correct name
|
|
self.assertFalse(ra['foo'].is_view)
|
|
self.assertEqual(0, len(ra['foo']))
|
|
self.assertEqual('foo', ra['foo'].name)
|
|
# Key must be a string
|
|
with self.assertRaises(TypeError):
|
|
# noinspection PyTypeChecker
|
|
_ = ra[42]
|
|
|
|
def test_requestarguments_attr(self):
|
|
"""
|
|
Test attribute access syntactic sugar.
|
|
"""
|
|
ra = RequestArguments()
|
|
# Attribute should not exist yet
|
|
with self.assertRaises(KeyError):
|
|
_ = ra.foo
|
|
# Create entry
|
|
_ = ra['foo']
|
|
# Creating entry should have created the attribute
|
|
self.assertEqual('foo', ra.foo.name)
|
|
# Attribute access should yield an immutable view
|
|
self.assertTrue(ra.foo.is_view)
|
|
|
|
def test_requestarguments_iterate(self):
|
|
"""
|
|
Test iterating a RequestArguments instance.
|
|
"""
|
|
# Create an instance with some values
|
|
ra = RequestArguments()
|
|
ra['foo'].append('a', 'b')
|
|
ra['bar'].append('c', 'd')
|
|
ra['foo'].append('e', 'f')
|
|
# Container for test values (name, value)
|
|
items: Set[Tuple[str, str]] = set()
|
|
# Iterate RequestArguments instance, adding the name and value of each to the set
|
|
for a in ra:
|
|
items.add((a.name, str(a)))
|
|
# Compare result with expected value
|
|
self.assertEqual(2, len(items))
|
|
self.assertIn(('foo', 'b'), items)
|
|
self.assertIn(('bar', 'd'), items)
|
|
|
|
def test_requestarguments_full_use_case(self):
|
|
"""
|
|
Simulate a minimal RequestArguments use case.
|
|
"""
|
|
# Create empty RequestArguments instance
|
|
ra = RequestArguments()
|
|
# Parse GET request
|
|
getargs: Dict[str, List[str]] = urllib.parse.parse_qs('foo=42&bar=1337&foo=43&baz=Hello,%20World!')
|
|
# Insert GET arguments into RequestArguments
|
|
for k, vs in getargs.items():
|
|
for v in vs:
|
|
ra[k].append('text/plain', v)
|
|
# Parse POST request
|
|
postargs: Dict[str, List[str]] = urllib.parse.parse_qs('foo=postfoo&postbar=42&foo=postfoo')
|
|
# Insert POST arguments into RequestArguments
|
|
for k, vs in postargs.items():
|
|
# In this implementation, POST args replace GET args
|
|
ra[k].clear()
|
|
for v in vs:
|
|
ra[k].append('text/plain', v)
|
|
|
|
# Someplace else: Use the RequestArguments instance.
|
|
self.assertEqual('1337', ra.bar.get_str())
|
|
self.assertEqual('Hello, World!', ra.baz.get_str())
|
|
self.assertEqual('42', ra.postbar.get_str())
|
|
for a in ra.foo:
|
|
self.assertEqual('postfoo', a.get_str())
|