From d5a015372637f022d8a915ed51c2fc41dfbce7d4 Mon Sep 17 00:00:00 2001 From: Craig Knudsen Date: Mon, 11 Sep 2023 15:17:55 -0400 Subject: [PATCH] Fix for auto-update to new version when no db changes required. Added some pytest code. - New pytest code ui-tests.py requires WebCalendar to be running with a default admin account/password). I am hoping to add to this and eventually add it to the GitHub actions so that there is some additional testing provided for merges and PRs. --- includes/functions.php | 81 +++++++++++- install/sql/upgrade_matrix.php | 2 +- tests/functionsTest.php | 5 +- tests/ui-tests.py | 224 +++++++++++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 tests/ui-tests.py diff --git a/includes/functions.php b/includes/functions.php index d9afc4fa3..ab376450f 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -6474,6 +6474,53 @@ function sendCookie($name, $value, $expiration=0, $path='', $sensitive=true) { SetCookie ( $name, $value, $expiration, $path, $domain, $secure, $httpOnly); } +/** + * Finds the next version greater than the specified version from a given file content. + * + * The function scans the provided file content to detect version patterns like "upgrade_v1.9.5". + * It then compares the found versions to the provided version and returns the immediate next version + * greater than the provided one. + * + * @param string $fileContent The content of the file containing version upgrade markers. + * @param string $version The version to be compared against. + * + * @return string The next version greater than the provided version, or an empty string if not found. + */ +function findNextVersion($fileContent, $version) { + // Extract all versions from the file content + preg_match_all('/\/\*upgrade_(v\d+\.\d+\.\d+)\*\//', $fileContent, $matches); + $versions = $matches[1]; + + // Removing the "v" prefix from versions + $version_trimmed = ltrim($version, "v"); + + for ($i = 0; $i < count($versions); $i++) { + $version2 = ltrim($versions[$i], "v"); + if (version_compare($version_trimmed, $version2, '<')) { + return "v$version2"; + } + } + return ''; // Not found +} + +/** + * Determines if a software upgrade requires database changes based on the database type and version range. + * + * The function inspects the SQL upgrade file associated with the specified database type to find SQL changes + * between the provided old and new version. If there's any significant SQL content between these version + * markers, it indicates that database changes are required. + * + * For some database types where the SQL upgrade file might not exist, it falls back to checking the MySQL + * upgrade file as a general reference. + * + * @param string $db_type The type of the database (e.g., 'mysql', 'mysqli', 'postgres'). + * @param string $old_version The starting version to check for changes from (e.g., 'v1.9.0'). + * @param string $new_version The ending version to check for changes up to (e.g., 'v1.9.5'). + * + * @return bool True if SQL changes are found between the versions, false otherwise. + * + * @throws Exception Throws an exception if the SQL file for the specified database type doesn't exist. + */ function upgrade_requires_db_changes($db_type, $old_version, $new_version) { // File path if ($db_type == 'mysqli') @@ -6494,18 +6541,48 @@ function upgrade_requires_db_changes($db_type, $old_version, $new_version) { // Get the SQL content between this version and the next $start_token = "/*upgrade_$old_version*/"; - $start_pos = strpos($content, "$start_token") + strlen($start_token); + $start_pos = strpos($content, "$start_token"); + if ($start_pos) { + $start_pos += strlen($start_token);; + } else { + // Not found. Find the next version up. + $next_version = findNextVersion($content, $old_version); + if (empty($next_version)) { // shouldn't happen unless file is messed up + return true; + } + $start_token = "/*upgrade_$next_version*/"; + $start_pos = strpos($content, "$start_token"); + if (!$start_pos) { + return true; + } + $start_pos += strlen($start_token); + } $end_token = "/*upgrade_$new_version*/"; $end_pos = strpos($content, "$end_token"); $sql_content = trim(substr($content, $start_pos, $end_pos - $start_pos)); + $no_comments = preg_replace('/\/\*upgrade_v\d+\.\d+\.\d+\*\/\n?/', '', $sql_content); // Check if there's more than just the comment (meaning there are SQL commands) - if (strlen($sql_content) > 10) { + if (strlen($no_comments) > 10) { return true; // Found SQL changes }; return false; // No SQL changes found } +/** + * Updates the WebCalendar version in the database and logs the update activity. + * + * This function modifies the 'webcal_config' table to set the new WebCalendar version. + * Additionally, an activity log is created to keep track of the update and which user + * performed the update. + * + * @param string $old_version The current version before the update (e.g., 'v1.9.0'). + * @param string $new_version The desired new version after the update (e.g., 'v1.9.5'). + * + * @global object $user The global user object representing the current logged-in user. + * + * @return bool True if the version update is successful, false otherwise. + */ function update_webcalendar_version_in_db($old_version, $new_version) { global $user; if (!dbi_execute('UPDATE webcal_config SET cal_value = ? WHERE cal_setting = ?', diff --git a/install/sql/upgrade_matrix.php b/install/sql/upgrade_matrix.php index 155a23f21..bc1fb3a7a 100644 --- a/install/sql/upgrade_matrix.php +++ b/install/sql/upgrade_matrix.php @@ -89,7 +89,7 @@ // Should get MySQL error: Column 'cat_owner' cannot be null ['INSERT INTO webcal_entry_categories (cal_id, cat_id, cat_order, cat_owner) VALUES (999999, 1, -1, NULL)', 'DELETE FROM webcal_entry_categories WHERE cal_id = 999999 AND cat_order = -1', - 'v1.3.0', 'upgrade_v1.9.0'], + 'v1.3.0', 'upgrade_v1.9.6'], //don't change this array element ['','', $PROGRAM_VERSION, ''] ]; diff --git a/tests/functionsTest.php b/tests/functionsTest.php index 345429155..7c31250cd 100644 --- a/tests/functionsTest.php +++ b/tests/functionsTest.php @@ -194,8 +194,9 @@ function test_upgrade_requires_db_changes() { $this->assertTrue(upgrade_requires_db_changes('mysql', 'v1.3.0', 'v1.9.1')); $this->assertFalse(upgrade_requires_db_changes('mysql', 'v1.9.1', 'v1.9.2')); $this->assertFalse(upgrade_requires_db_changes('mysql', 'v1.9.2', 'v1.9.5')); - $this->assertTrue(upgrade_requires_db_changes('mysql', 'v1.9.5', 'v1.9.7')); - $this->assertTrue(upgrade_requires_db_changes('mysql', 'v1.3.0', 'v1.9.7')); + $this->assertTrue(upgrade_requires_db_changes('mysql', 'v1.9.5', 'v1.9.8')); + $this->assertFalse(upgrade_requires_db_changes('mysql', 'v1.9.7', 'v1.9.8')); + $this->assertTrue(upgrade_requires_db_changes('mysql', 'v1.3.0', 'v1.9.8')); } } diff --git a/tests/ui-tests.py b/tests/ui-tests.py new file mode 100644 index 000000000..52f2d6dff --- /dev/null +++ b/tests/ui-tests.py @@ -0,0 +1,224 @@ +import httpx +from lxml import html +import pytest +from datetime import datetime +import uuid + +BASE_URL = "http://localhost:8080" +ADMIN_USERNAME = "admin" +ADMIN_PASSWORD = "admin" + +@pytest.fixture(scope="module") +def http_client(): + with httpx.Client() as client: + # Load the login page + response = client.get(f"{BASE_URL}/login.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="user"]') + assert tree.xpath('//input[@id="password"]') + + # Login with the given credentials + login_data = { + "login": ADMIN_USERNAME, + "password": ADMIN_PASSWORD + } + login_response = client.post(f"{BASE_URL}/login.php", data=login_data) + + # Ensure the login was successful + assert login_response.status_code == 302 + yield client # This client will be used in the tests and it is already authenticated + + +def test_add_event_missing_csrf(http_client): + # Load the edit_entry page + response = http_client.get(f"{BASE_URL}/edit_entry.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="entry_brief"]') + assert tree.xpath('//input[@id="_YMD"]') + + # Set the description and date (20091025) + current_date = datetime.now() + formatted_date = current_date.strftime('%Y%m%d') + login_data = { + "_YMD": formatted_date, + "name": "Sample Event #1" + } + add_response = http_client.post(f"{BASE_URL}/edit_entry_handler.php", data=login_data) + + # Successful save will give 302 redirect to month.php + assert add_response.status_code == 200 # which means that save failed + assert "Invalid form request" in add_response.text + print(add_response.text) + # TODO: Add additional cases that should fail: Missing brief descriptions, invalid dates, permission denied, + # invalid or missing timetype, etc. + + +def test_add_event(http_client): + # Load the edit_entry page + response = http_client.get(f"{BASE_URL}/edit_entry.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="entry_brief"]') + assert tree.xpath('//input[@id="_YMD"]') + csrf_token = tree.xpath('//input[@name="csrf_form_key"]/@value')[0] + assert csrf_token + + # Set the description and date (20091025) + current_date = datetime.now() + formatted_date = current_date.strftime('%Y%m%d') + # Generate a random UUID + event_uuid = str(uuid.uuid4()) + event_name = "Event " + event_uuid + login_data = { + "csrf_form_key": csrf_token, + "_YMD": formatted_date, + "name": event_name, + "timetype": "U" + } + add_response = http_client.post(f"{BASE_URL}/edit_entry_handler.php", data=login_data) + + # Successful save will give 302 redirect to month.php + assert add_response.status_code == 302 + print(add_response.text) + +def test_get_event(http_client): + # Now use activity_log.php to see if the event shows up + response = httpx.get(f"{BASE_URL}/activity_log.php") + content = response.text + print(response.text) + # Parse the HTML + tree = html.fromstring(content) + import httpx +from lxml import html +import pytest +from datetime import datetime +import uuid + +BASE_URL = "http://localhost:8080" +ADMIN_USERNAME = "admin" +ADMIN_PASSWORD = "admin" + +@pytest.fixture(scope="module") +def http_client(): + with httpx.Client() as client: + # Load the login page + response = client.get(f"{BASE_URL}/login.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="user"]') + assert tree.xpath('//input[@id="password"]') + + # Login with the given credentials + login_data = { + "login": ADMIN_USERNAME, + "password": ADMIN_PASSWORD + } + login_response = client.post(f"{BASE_URL}/login.php", data=login_data) + + # Ensure the login was successful + assert login_response.status_code == 302 + yield client # This client will be used in the tests and it is already authenticated + + +def test_add_event_missing_csrf(http_client): + # Load the edit_entry page + response = http_client.get(f"{BASE_URL}/edit_entry.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="entry_brief"]') + assert tree.xpath('//input[@id="_YMD"]') + + # Set the description and date (20091025) + current_date = datetime.now() + formatted_date = current_date.strftime('%Y%m%d') + login_data = { + "_YMD": formatted_date, + "name": "Sample Event #1" + } + add_response = http_client.post(f"{BASE_URL}/edit_entry_handler.php", data=login_data) + + # Successful save will give 302 redirect to month.php + assert add_response.status_code == 200 # which means that save failed + assert "Invalid form request" in add_response.text + print(add_response.text) + # TODO: Add additional cases that should fail: Missing brief descriptions, invalid dates, permission denied, + # invalid or missing timetype, etc. + + +def test_add_event(http_client): + # Load the edit_entry page + response = http_client.get(f"{BASE_URL}/edit_entry.php") + assert response.status_code == 200 + + # Parse the HTML content + tree = html.fromstring(response.text) + + # Check if the form elements exist + assert tree.xpath('//input[@id="entry_brief"]') + assert tree.xpath('//input[@id="_YMD"]') + csrf_token = tree.xpath('//input[@name="csrf_form_key"]/@value')[0] + assert csrf_token + + # Set the description and date (20091025) + current_date = datetime.now() + formatted_date = current_date.strftime('%Y%m%d') + # Generate a random UUID + event_uuid = str(uuid.uuid4()) + event_name = "Event " + event_uuid + login_data = { + "csrf_form_key": csrf_token, + "_YMD": formatted_date, + "name": event_name, + "timetype": "U" + } + add_response = http_client.post(f"{BASE_URL}/edit_entry_handler.php", data=login_data) + + # Successful save will give 302 redirect to month.php + assert add_response.status_code == 302 + print(add_response.text) + +def test_get_event(http_client): + # Now use activity_log.php to see if the event shows up + response = http_client.get(f"{BASE_URL}/activity_log.php?1") + content = response.text + #print(response.text) + # Parse the HTML + tree = html.fromstring(content) + url_list = tree.xpath('//a[starts-with(@title, "Event ")]/@href') + assert url_list + assert len(url_list) >= 1 + assert url_list + assert "view_entry.php" in url_list[0] + #print("url_list: " + str(url_list)) + url=url_list[0] + + # Load the page now + response = http_client.get(f"{BASE_URL}/{url}") + assert response.status_code == 200 + content = response.text + print(content) + assert 'id="view-event-title"' in content + assert '' in content + #TODO: verify content of event details