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())