added the beginnings of a suite of video-related tests

This commit is contained in:
cwshugg 2022-03-22 00:05:09 -04:00
parent 74e9d678f9
commit 859c3d0e6c

View File

@ -1981,6 +1981,227 @@ class Authentication(Doc_Print_Test_Case):
# Close the session # Close the session
self.sessions[i].close() self.sessions[i].close()
class VideoStreaming(Doc_Print_Test_Case):
"""
Test cases for the /api/video endpoint and using Range requests to stream
videos and other files.
"""
def __init__(self, testname, hostname, port):
"""
Prepare the test case for creating connections.
"""
super(VideoStreaming, self).__init__(testname)
self.hostname = hostname
self.port = port
self.nonvideos = ['index.html', 'js/jquery.min.js', 'css/jquery-ui.min.css']
def setUp(self):
""" Test Name: None -- setUp function\n\
Number Connections: N/A \n\
Procedure: Opens the HTTP connection to the server. An error here \
means the script was unable to create a connection to the \
server.
"""
# open the base directory and search through it for video files. We'll
# use these to compare against /api/video responses
self.vids = []
for root, dirs, files in os.walk(base_dir):
for fname in files:
# only consider .mp4 files
if fname.endswith(".mp4"):
self.vids.append(os.path.join(root, fname))
# Create a requests session
self.session = requests.Session()
def tearDown(self):
""" Test Name: None -- tearDown function\n\
Number Connections: N/A \n\
Procedure: Closes the HTTP connection to the server. An error here \
means the server crashed after servicing the request from \
the previous test.
"""
# Close the HTTP connection
self.session.close()
# Does a lower-case search for headers within a response's headers. If
# found, the first ocurrence is returned (the header's value is returned).
def find_header(self, response, name):
for header in response.headers:
if header.lower() == name.lower():
return response.headers[header]
return None
def test_api_video(self):
""" Test Name: test_api_video
Number Connections: N/A
Procedure: Tests the /api/video endpoint and ensures the server responds
with the correct information. A failure here means either /api/video is
unsupported entirely or there's an issue with the JSON data the server
send in a response to GET /api/video.
"""
# build the URL for /api/video and make the GET request
url = "http://%s:%s/api/video" % (self.hostname, self.port)
response = None
# make a GET request for the file
try:
req = requests.Request('GET', url)
prepared_req = req.prepare()
prepared_req.url = url
response = self.session.send(prepared_req, timeout=2)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
# make sure the correct status code was received
if response.status_code != requests.codes.ok:
raise AssertionError("Server responded with %d instead of 200 OK when requested with /api/video" %
response.status_code)
# make sure the correct Content-Type is specified
content_type = self.find_header(response, "Content-Type")
content_expect = "application/json"
if content_type == None:
raise AssertionError("Server didn't respond with the Content-Type header when requested with /api/video")
if content_type.lower() != content_expect:
raise AssertionError("Server didn't respond with the correct Content-Type value when requested with /api/video. "
"Expected: %s, received: %s" % (content_expect, content_type))
# attempt to decode the JSON data from /api/video
jdata = None
try:
jdata = response.json()
except Exception as e:
raise AssertionError("Failed to decode the JSON data your server sent as a response to /api/video. "
"Error: %s" % str(e))
# now we'll examine the JSON data and make sure it's formatted right
# [{"name": "v1.mp4", "size": 1512799}, {"name": "v2.mp4", "size": 9126406}]
if type(jdata) != list:
raise AssertionError("JSON data returned from /api/video must be in a list format (ex: [{\"a\": 1}, {\"b\": 2}])")
for entry in jdata:
# each entry should have two fields: "name" and "size"
if "name" not in entry or "size" not in entry:
raise AssertionError("Each JSON entry returned from /api/video must have a \"name\" and a \"size\"")
# next, iterate over the videos *we* observed in the test root
# directory and ensure each one is present in the JSON data
base_dir_full = os.path.realpath(base_dir)
for expected in self.vids:
# stat the video file to retrieve its size in bytes
expected_size = os.path.getsize(expected)
# use the base directory to derive the correct string that should
# be placed in the JSON data
expected_str = expected.replace(base_dir_full, "")
if expected_str.startswith("/"):
expected_str = expected_str[1:]
# search for the entry within the array, and thrown an error of we
# couldn't find it
entry = None
for e in jdata:
entry = e if e["name"] == expected_str else entry
if entry == None:
raise AssertionError("Failed to find \"%s\" in server's response to GET /api/video. Received:\n%s" %
(expected_str, json.dumps(jdata)))
# ensure the reported size is what we expect
if entry["size"] != expected_size:
raise AssertionError("Incorrect size reported for \"%s\" in response to GET /api/video. Expected %d, received %d" %
(expected_str, expected_size, entry["size"]))
def test_accept_ranges_header(self):
""" Test Name: test_accept_ranges_header
Number Connections: N/A
Procedure: Makes a variety of requests to the web server and searches
for at least one occurrence of the Accept-Ranges header being sent back
in the server's response headers. A failure here means the server doesn't
appear to be sending the Accept-Ranges header in its responses.
"""
# build a collection of URLs to try
url_prefix = "http://%s:%s" % (self.hostname, self.port)
resources = ["/", "/index.html", "/public/index.html", "/api/login", "/api/video", "/v1.mp4"]
# do the following for each URL
occurrences = 0
for resource in resources:
url = url_prefix + resource
response = None
# make a GET request for the particular resource
try:
req = requests.Request('GET', url)
prepared_req = req.prepare()
prepared_req.url = url
response = self.session.send(prepared_req, timeout=2)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
# make sure the correct status code was received
if response.status_code != requests.codes.ok:
raise AssertionError("Server responded with %d instead of 200 OK when requested with %s" %
response.status_code, resource)
# search the header dictionary (lowercase comparison) for Accept-Ranges
accept_ranges_expect = "bytes"
accept_ranges = self.find_header(response, "Accept-Ranges")
if accept_ranges != None:
occurrences += 1
# make sure the correct value is given ("bytes")
if "bytes" not in accept_ranges:
raise AssertionError("Server responded with an unexpected Accept-Ranges values. "
"Expected: %s, received: %s" % (accept_ranges_expect, response.headers[header]))
# if no occurrences were found, throw an error
if occurrences == 0:
raise AssertionError("Failed to find the Accept-Ranges header in the server's responses.")
def test_video_get(self):
""" Test Name: test_video_get
Number Connections: N/A
Procedure: Makes a simple GET request for a video and checks headers,
content length, bytes, etc. A failure here means GET requests for
videos (WITHOUT Range requests) aren't performing properly.
"""
# build a url to one of the videos in the test directory, then make a
# simple GET request
vid = os.path.basename(self.vids[0])
url = "http://%s:%s/%s" % (self.hostname, self.port, vid)
response = None
try:
req = requests.Request('GET', url)
prepared_req = req.prepare()
prepared_req.url = url
response = self.session.send(prepared_req, timeout=2)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
# make sure the correct status code was received
if response.status_code != requests.codes.ok:
raise AssertionError("Server responded with %d instead of 200 OK when requested with a valid video" %
response.status_code)
# check for the content-type header
content_type = self.find_header(response, "Content-Type")
content_expect = "video/mp4"
if content_type == None:
raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video")
if content_type.lower() != content_expect:
raise AssertionError("Server didn't respond with the correct Content-Type value when requested with a valid video. "
"Expected: %s, received: %s" % (content_expect, content_type))
# check for the content-length header
content_length = self.find_header(response, "Content-Length")
content_length_expect = str(os.path.getsize(self.vids[0]))
if content_length == None:
raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video")
if content_length != content_length_expect:
raise AssertionError("Server didn't respond with the correct Content-Length value when requested with a valid video. "
"Expected: %s, received: %s" % (content_length_expect, content_length))
# TODO finish
############################################################################### ###############################################################################
# Globally define the Server object so it can be checked by all test cases # Globally define the Server object so it can be checked by all test cases
############################################################################### ###############################################################################
@ -2015,22 +2236,25 @@ grade_points_available = 95
# 6 tests # 6 tests
minreq_total = 25 minreq_total = 25
# 27 tests # 27 tests
extra_total = 20 extra_total = 15
# 5 tests # 5 tests
malicious_total = 20 malicious_total = 15
# 4 tests # 4 tests
ipv6_total = 5 ipv6_total = 5
# ? tests # ? tests
auth_total = 20 auth_total = 20
# ? tests (html5 fallback) # ? tests (html5 fallback)
fallback_total = 5 fallback_total = 5
# ? tests (video features)
video_total = 10
def print_points(minreq, extra, malicious, ipv6, auth, fallback): def print_points(minreq, extra, malicious, ipv6, auth, fallback, video):
"""All arguments are fractions (out of 1)""" """All arguments are fractions (out of 1)"""
print("Minimum Requirements: \t%2d/%2d" % (int(minreq * minreq_total), minreq_total)) print("Minimum Requirements: \t%2d/%2d" % (int(minreq * minreq_total), minreq_total))
print("Authentication Functionality: \t%2d/%2d" % (int(auth * auth_total), auth_total)) print("Authentication Functionality: \t%2d/%2d" % (int(auth * auth_total), auth_total))
print("HTML5 Fallback Functionality: \t%2d/%2d" % (int(fallback * fallback_total), fallback_total)) print("HTML5 Fallback Functionality: \t%2d/%2d" % (int(fallback * fallback_total), fallback_total))
print("Video Functionality: \t%2d/%2d" % (int(video * video_total), video_total))
print("IPv6 Functionality: \t%2d/%2d" % (int(ipv6 * ipv6_total), ipv6_total)) print("IPv6 Functionality: \t%2d/%2d" % (int(ipv6 * ipv6_total), ipv6_total))
print("Extra Tests: \t%2d/%2d" % (int(extra * extra_total), extra_total)) print("Extra Tests: \t%2d/%2d" % (int(extra * extra_total), extra_total))
print("Robustness: \t%2d/%2d" % (int(malicious * malicious_total), malicious_total)) print("Robustness: \t%2d/%2d" % (int(malicious * malicious_total), malicious_total))
@ -2080,7 +2304,7 @@ if __name__ == '__main__':
alltests = [Single_Conn_Good_Case, Multi_Conn_Sequential_Case, Single_Conn_Bad_Case, alltests = [Single_Conn_Good_Case, Multi_Conn_Sequential_Case, Single_Conn_Bad_Case,
Single_Conn_Malicious_Case, Single_Conn_Protocol_Case, Access_Control, Single_Conn_Malicious_Case, Single_Conn_Protocol_Case, Access_Control,
Authentication, Fallback] Authentication, Fallback, VideoStreaming]
def findtest(tname): def findtest(tname):
@ -2242,6 +2466,13 @@ process.
if test_function.startswith("test_"): if test_function.startswith("test_"):
html5_fallback_suite.addTest(Fallback(test_function, hostname, port)) html5_fallback_suite.addTest(Fallback(test_function, hostname, port))
# Test Suite for video streaming functionality. Add all tests from the
# VideoStreaming class.
video_suite = unittest.TestSuite()
for test_function in dir(VideoStreaming):
if test_function.startswith("test_"):
video_suite.addTest(VideoStreaming(test_function, hostname, port))
# Test Suite for extra points, mostly testing error cases # Test Suite for extra points, mostly testing error cases
extra_tests_suite = unittest.TestSuite() extra_tests_suite = unittest.TestSuite()
@ -2282,7 +2513,7 @@ process.
"Please examine the above errors, the remaining tests\n" + "Please examine the above errors, the remaining tests\n" +
"will not be run until after the above tests pass.\n") "will not be run until after the above tests pass.\n")
print_points(minreq_score, 0, 0, 0, 0, 0) print_points(minreq_score, 0, 0, 0, 0, 0, 0)
sys.exit() sys.exit()
print('Beginning Authentication Tests') print('Beginning Authentication Tests')
@ -2312,6 +2543,19 @@ process.
F(html5_fallback_suite.countTestCases() - len(test_results.errors) - len(test_results.failures), F(html5_fallback_suite.countTestCases() - len(test_results.errors) - len(test_results.failures),
html5_fallback_suite.countTestCases())) html5_fallback_suite.countTestCases()))
print('Beginning Video Streaming Tests')
# kill the server and restart it
killserver(server)
server.wait()
server = start_server()
time.sleep(3 if run_slow else 1)
# run the video streaming tests and compute a score
test_results = unittest.TextTestRunner().run(video_suite)
video_score = max(0,
F(video_suite.countTestCases() - len(test_results.errors) - len(test_results.failures),
video_suite.countTestCases()))
def makeTestSuiteForHost(hostname): def makeTestSuiteForHost(hostname):
# IPv6 Test Suite # IPv6 Test Suite
ipv6_test_suite = unittest.TestSuite() ipv6_test_suite = unittest.TestSuite()
@ -2395,7 +2639,7 @@ process.
"Please examine the above errors, the Malicious Tests\n" + "Please examine the above errors, the Malicious Tests\n" +
"will not be run until the above tests pass.\n") "will not be run until the above tests pass.\n")
print_points(minreq_score, extra_score, 0, ipv6_score, auth_score, fallback_score) print_points(minreq_score, extra_score, 0, ipv6_score, auth_score, fallback_score, video_score)
sys.exit() sys.exit()
print("Now running the MALICIOUS Tests. WARNING: These tests will not necessarily run fast!") print("Now running the MALICIOUS Tests. WARNING: These tests will not necessarily run fast!")
@ -2414,5 +2658,5 @@ process.
print("\nYou have NOT passed one or more of the Malicious Tests. " + print("\nYou have NOT passed one or more of the Malicious Tests. " +
"Please examine the errors listed above.\n") "Please examine the errors listed above.\n")
print_points(minreq_score, extra_score, robustness_score, ipv6_score, auth_score, fallback_score) print_points(minreq_score, extra_score, robustness_score, ipv6_score, auth_score, fallback_score, video_score)