diff --git a/.github/workflows/Unit-Tests.yml b/.github/workflows/Unit-Tests.yml new file mode 100644 index 0000000..b9633a1 --- /dev/null +++ b/.github/workflows/Unit-Tests.yml @@ -0,0 +1,30 @@ +name: Run Blackboard Session Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install backend dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + + - name: Run tests + run: | + python backend/test_blackboard_scraper.py diff --git a/backend/blackboard_scraper.py b/backend/blackboard_scraper.py index 0817d5e..81d4357 100644 --- a/backend/blackboard_scraper.py +++ b/backend/blackboard_scraper.py @@ -84,6 +84,12 @@ def set_response(self, response): def get_response(self): return self.response + + def get_InstructorsFound(self): + return self.instructorsFound + + def set_InstructorsFound(self, instructorsFound): + self.instructorsFound = instructorsFound def shutdown(self): """ @@ -159,7 +165,7 @@ def scrape(self): return self.enable_instructors() - if self.instructorsFound == False: + if self.get_InstructorsFound() == False: self.response = "No instructors found." self.get_courses() @@ -189,70 +195,76 @@ def enable_instructors(self): try: get_url = "https://kettering.blackboard.com/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1&forwardUrl=edit_module%2F_4_1%2Fbbcourseorg%3Fcmd%3Dedit&recallUrl=%2Fwebapps%2Fportal%2Fexecute%2Ftabs%2FtabAction%3Ftab_tab_group_id%3D_1_1" - get_response = self._get_request(get_url) - - if get_response.status_code != 200: - raise Exception("GET request failed.") - - # Using beautiful soup get the value from this input #moduleEditForm > input[type=hidden]:nth-child(1) - soup = BeautifulSoup(get_response.content, "html.parser") - nonce_value = soup.select_one( - '#moduleEditForm > input[type=hidden]:nth-child(1)')['value'] - - url = "https://kettering.blackboard.com/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1&forwardUrl=proc_edit/_4_1/bbcourseorg&recallUrl=%2Fwebapps%2Fportal%2Fexecute%2Ftabs%2FtabAction%3Ftab_tab_group_id%3D_1_1" - payload = { - 'tab_tab_group_id': '_1_1', - 'forwardUrl': 'proc_edit/_4_1/bbcourseorg', - 'blackboard.platform.security.NonceUtil.nonce': nonce_value, - 'recallUrl': '/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1', - 'cmd': 'processEdit', - 'serviceLevel': '', - 'termDisplayOrder': '_254_1', - 'amc.groupbyterm': 'true', - 'selectAll_254_1': 'true', - 'amc.showterm._254_1': 'true', - 'termCourses__254_1': 'true', - 'amc.showcourse._51671_1': 'true', - 'amc.showcourseid._51671_1': 'true', - 'amc.showinstructors._51671_1': 'true', - 'amc.showcourse._51672_1': 'true', - 'amc.showcourseid._51672_1': 'true', - 'amc.showinstructors._51672_1': 'true', - 'amc.showcourse._51629_1': 'true', - 'amc.showcourseid._51629_1': 'true', - 'amc.showinstructors._51629_1': 'true', - 'amc.showcourse._51904_1': 'true', - 'amc.showcourseid._51904_1': 'true', - 'amc.showinstructors._51904_1': 'true', - 'amc.showcourse._51945_1': 'true', - 'amc.showcourseid._51945_1': 'true', - 'amc.showinstructors._51945_1': 'true', - 'amc.url.name.1': '', - 'amc.url.url.1': '', - 'amc.url.name.2': '', - 'amc.url.url.2': '', - 'amc.url.name.3': '', - 'amc.url.url.3': '', - 'amc.url.name.4': '', - 'amc.url.url.4': '', - 'amc.url.name.5': '', - 'amc.url.url.5': '', - 'bottom_Submit': 'Submit' - } - enable_instructors_response = self._send_post_request( - url, data=payload, allow_redirects=False) - - if enable_instructors_response.status_code == 302: - redirected_url = enable_instructors_response.headers['Location'] - logging.info( - f"Successful POST request. Redirected to: {redirected_url}") - self.instructorsFound = True - else: - self.instructorsFound = False + try: + get_response = self._get_request(get_url) + + if get_response.status_code != 200: + raise Exception("GET request failed.") + + # Using beautiful soup get the value from this input #moduleEditForm > input[type=hidden]:nth-child(1) + soup = BeautifulSoup(get_response.content, "html.parser") + nonce_value = soup.select_one( + '#moduleEditForm > input[type=hidden]:nth-child(1)')['value'] + + url = "https://kettering.blackboard.com/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1&forwardUrl=proc_edit/_4_1/bbcourseorg&recallUrl=%2Fwebapps%2Fportal%2Fexecute%2Ftabs%2FtabAction%3Ftab_tab_group_id%3D_1_1" + payload = { + 'tab_tab_group_id': '_1_1', + 'forwardUrl': 'proc_edit/_4_1/bbcourseorg', + 'blackboard.platform.security.NonceUtil.nonce': nonce_value, + 'recallUrl': '/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1', + 'cmd': 'processEdit', + 'serviceLevel': '', + 'termDisplayOrder': '_254_1', + 'amc.groupbyterm': 'true', + 'selectAll_254_1': 'true', + 'amc.showterm._254_1': 'true', + 'termCourses__254_1': 'true', + 'amc.showcourse._51671_1': 'true', + 'amc.showcourseid._51671_1': 'true', + 'amc.showinstructors._51671_1': 'true', + 'amc.showcourse._51672_1': 'true', + 'amc.showcourseid._51672_1': 'true', + 'amc.showinstructors._51672_1': 'true', + 'amc.showcourse._51629_1': 'true', + 'amc.showcourseid._51629_1': 'true', + 'amc.showinstructors._51629_1': 'true', + 'amc.showcourse._51904_1': 'true', + 'amc.showcourseid._51904_1': 'true', + 'amc.showinstructors._51904_1': 'true', + 'amc.showcourse._51945_1': 'true', + 'amc.showcourseid._51945_1': 'true', + 'amc.showinstructors._51945_1': 'true', + 'amc.url.name.1': '', + 'amc.url.url.1': '', + 'amc.url.name.2': '', + 'amc.url.url.2': '', + 'amc.url.name.3': '', + 'amc.url.url.3': '', + 'amc.url.name.4': '', + 'amc.url.url.4': '', + 'amc.url.name.5': '', + 'amc.url.url.5': '', + 'bottom_Submit': 'Submit' + } + enable_instructors_response = self._send_post_request( + url, data=payload, allow_redirects=False) + + if enable_instructors_response.status_code == 302: + redirected_url = enable_instructors_response.headers['Location'] + logging.info( + f"Successful POST request. Redirected to: {redirected_url}") + self.set_InstructorsFound(True) + else: + self.set_InstructorsFound(False) + logging.error( + f"POST request failed with status code: {enable_instructors_response.status_code}") + + self.last_activity_time = time.time() + + except Exception as e: logging.error( - f"POST request failed with status code: {enable_instructors_response.status_code}") - - self.last_activity_time = time.time() + f"GET request failed with status code: {get_response.status_code}") + return except Exception as e: logging.error(f"An error occurred enabling instructors: {e}") @@ -304,7 +316,7 @@ def get_courses(self): hrefs = {course.text.strip(): course.find("a")["href"].strip() for course in courses_list if course.find("a") and course.find("a").get("href")} - if self.instructorFound: + if self.get_InstructorsFound() == True: # Process instructors and format course names for course in courses_list: try: @@ -337,6 +349,7 @@ def get_courses(self): except Exception as e: self.courseFound = False + self.response = e logging.error(f"An error occurred while getting courses: {e}") def download_and_save_file(self): @@ -470,6 +483,3 @@ def process_course(course, href): self.download_tasks = download_tasks self.downloadTasksFound = True self.last_activity_time = time.time() - - - diff --git a/backend/requirements.txt b/backend/requirements.txt index 3c16ea3..d5177b0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,9 @@ ghostscript selenium flask -ray argparse bs4 gunicorn flask_cors flask_apscheduler -pydrive2 \ No newline at end of file +pydrive2 diff --git a/backend/test_blackboard_scraper.py b/backend/test_blackboard_scraper.py index 87479ad..b86ab67 100644 --- a/backend/test_blackboard_scraper.py +++ b/backend/test_blackboard_scraper.py @@ -1,12 +1,15 @@ -import logging import random +import time import unittest -from backend.blackboard_scraper import BlackboardSession -from unittest.mock import MagicMock, patch +from blackboard_scraper import BlackboardSession +from unittest.mock import patch from usernames import usernames + class TestBlackboardSession(unittest.TestCase): + # * Login Tests *# + def test_valid_credentials_login(self): # Set up username = 'Free8864' @@ -14,14 +17,15 @@ def test_valid_credentials_login(self): session = BlackboardSession(username=username, password=password) # Execute login - response = session.login() + session.login() + + response = session.get_response() # Check the response expected_message = "Login successful." self.assertEqual(response, expected_message) - def test_invalid_both_login(self): # Set up username = 'InvalidUsername' @@ -29,24 +33,28 @@ def test_invalid_both_login(self): session = BlackboardSession(username=username, password=password) # Execute login - response = session.login() + session.login() + + response = session.get_response() # Check the response expected_error_message = "The username you entered cannot be identified." self.assertEqual(response, expected_error_message) - def test_failed_login_invalid_password(self): # selected a random username from usernames.py username = random.choice(list(usernames)) invalid_password = 'InvalidPassword' - session = BlackboardSession(username=username, password=invalid_password) + session = BlackboardSession( + username=username, password=invalid_password) # Execute login - response = session.login() + session.login() + + response = session.get_response() # Check the response error_messages = [ @@ -55,21 +63,322 @@ def test_failed_login_invalid_password(self): self.assertTrue(response in error_messages) - def test_failed_login_invalid_username(self): # Set up invalid_username = 'InvalidUsername' password = 'InvalidPassword' - session = BlackboardSession(username=invalid_username, password=password) + session = BlackboardSession( + username=invalid_username, password=password) # Execute login - response = session.login() + session.login() + + response = session.get_response() # Check the response expected_error_message = "The username you entered cannot be identified." self.assertEqual(response, expected_error_message) + # * Enable Instructors *# + + def test_enable_instructors_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the GET request + with patch.object(session, '_get_request') as mock_get_request: + mock_get_request.return_value.status_code = 200 + mock_get_request.return_value.content = ''' + +
+ +
+ + ''' + + # Mock the POST request + with patch.object(session, '_send_post_request') as mock_post_request: + mock_post_request.return_value.status_code = 302 + mock_post_request.return_value.headers = { + 'Location': 'https://kettering.blackboard.com'} + + # Execute enable_instructors + session.enable_instructors() + + # Check the response + self.assertTrue(session.instructorsFound) + self.assertAlmostEqual( + session.last_activity_time, time.time(), delta=1) + + def test_enable_instructors_not_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = False + + # Execute enable_instructors + session.enable_instructors() + + # Check the response + self.assertEqual(session.response, "Not logged in.") + self.assertFalse(session.instructorsFound) + self.assertIsNone(session.last_activity_time) + + def test_enable_instructors_get_request_failed(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the GET request + with patch.object(session, '_get_request') as mock_get_request: + mock_get_request.return_value.status_code = 500 + + # Mock the logging.error function + with patch('logging.error') as mock_logging_error: + # Execute enable_instructors + session.enable_instructors() + + # Check the response + self.assertFalse(session.instructorsFound) + + # Check the logging.error call + mock_logging_error.assert_called_once_with( + f"GET request failed with status code: {mock_get_request.return_value.status_code}") + + def test_enable_instructors_post_request_failed(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the GET request + with patch.object(session, '_get_request') as mock_get_request: + mock_get_request.return_value.status_code = 200 + mock_get_request.return_value.content = ''' + +
+ +
+ + ''' + + # Mock the POST request + with patch.object(session, '_send_post_request') as mock_post_request: + mock_post_request.return_value.status_code = 500 + + # Mock the logging.error function + with patch('logging.error') as mock_logging_error: + # Execute enable_instructors + session.enable_instructors() + + # Check the response + self.assertFalse(session.instructorsFound) + + # Check the logging.error call + mock_logging_error.assert_called_once_with( + f"POST request failed with status code: {mock_post_request.return_value.status_code}") + + # * Get Courses *# + + def test_get_courses_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the POST request + with patch.object(session, '_send_post_request') as mock_post_request: + mock_post_request.return_value.status_code = 200 + mock_post_request.return_value.content = ''' + +
+ +
+ + ''' + + # Execute get_courses + session.get_courses() + + # Check the response + expected_courses = { + 'Course 1': 'course1_link', + 'Course 2': 'course2_link' + } + self.assertEqual(session.courses, expected_courses) + self.assertAlmostEqual( + session.last_activity_time, time.time(), delta=1) + + def test_get_courses_not_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = False + + # Execute get_courses + session.get_courses() + + # Check the response + self.assertEqual(session.response, "Not logged in.") + self.assertEqual(session.courses, {}) + self.assertIsNone(session.last_activity_time) + + def test_get_courses_no_courses(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the POST request + with patch.object(session, '_send_post_request') as mock_post_request: + mock_post_request.return_value.status_code = 200 + mock_post_request.return_value.content = ''' + +
+ +
+ + ''' + + # Execute get_courses + session.get_courses() + + # Check the response + self.assertEqual(session.response, + "You are not currently enrolled in any courses.") + self.assertEqual(session.courses, {}) + + def test_get_courses_error_finding_course_list(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + + # Mock the POST request + with patch.object(session, '_send_post_request') as mock_post_request: + mock_post_request.return_value.status_code = 500 + + # Mock the logging.error function + with patch('logging.error') as mock_logging_error: + # Execute get_courses + session.get_courses() + + # Check the response + self.assertIsInstance(session.response, Exception) + self.assertEqual(str(session.response), 'POST request failed.') + self.assertEqual(session.courses, {}) + mock_logging_error.assert_called_once() + + # * Get Download Tasks *# + + +def test_get_download_tasks_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = True + session.courses = { + 'Course 1': 'course1_link', + 'Course 2': 'course2_link' + } + + with patch.object(session, '_get_request') as mock_get_request: + mock_get_request.side_effect = [ + type('', (), {'status_code': 200, 'content': ''' + + +
+ +
+ + + '''}), + type('', (), {'status_code': 200, 'content': ''' + + +
+ +
+ + + '''}) + ] + + # Call the method + session.get_download_tasks() + + # Assert the result + expected_result = [ + ('Course 1', 'Assignment 1', 'download_link1'), + ('Course 1', 'Assignment 2', 'download_link2'), + ('Course 2', 'Assignment 1', 'download_link1'), + ('Course 2', 'Assignment 2', 'download_link2'), + ] + + self.assertEqual(session.download_tasks, expected_result) + self.assertTrue(session.downloadTasksFound) + self.assertAlmostEqual( + session.last_activity_time, time.time(), delta=1) + + def test_get_download_tasks_not_logged_in(self): + # Set up + username = 'Free8864' + password = '#CFi^F6TTwot2j' + session = BlackboardSession(username=username, password=password) + session.is_logged_in = False + + # Execute get_download_tasks + session.get_download_tasks() + + # Check the response + self.assertEqual(session.response, "Not logged in.") + self.assertFalse(session.downloadTasksFound) + self.assertIsNone(session.last_activity_time) + if __name__ == '__main__': unittest.main()