Skip to content

Commit

Permalink
Fix for auto-update to new version when no db changes required. Added…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
craigk5n committed Sep 11, 2023
1 parent df8fef6 commit d5a0153
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 5 deletions.
81 changes: 79 additions & 2 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 = ?',
Expand Down
2 changes: 1 addition & 1 deletion install/sql/upgrade_matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '']
];
Expand Down
5 changes: 3 additions & 2 deletions tests/functionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}

}
224 changes: 224 additions & 0 deletions tests/ui-tests.py
Original file line number Diff line number Diff line change
@@ -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 '<body id="viewentry">' in content
#TODO: verify content of event details

0 comments on commit d5a0153

Please sign in to comment.