Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions tests/e2e/playwright/browser-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const { chromium } = require('playwright');

function expectContains(haystack, needle, label) {
if (!haystack.includes(needle)) {
throw new Error(`Expected ${label} to contain: ${needle}`);
}
}

async function login(page) {
await page.goto('http://cacti_web/cacti/', { waitUntil: 'domcontentloaded' });
await page.locator('#login_username').fill('admin');
await page.locator('#login_password').fill('Admin123!');
await Promise.all([
page.waitForURL(/\/cacti\/index\.php/, { timeout: 30000 }),
page.locator('form#auth').evaluate((form) => form.submit())
]);
}

async function main() {
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
const page = await browser.newPage();

await login(page);

const pagePath = process.env.PAGE_PATH || 'index.php';
await page.goto(`http://cacti_web/cacti/${pagePath}`, { waitUntil: 'domcontentloaded' });

const title = await page.title();
const body = await page.locator('body').innerText();

if (process.env.EXPECT_TITLE) {
expectContains(title, process.env.EXPECT_TITLE, 'title');
}

for (const key of ['EXPECT_TEXT', 'EXPECT_TEXT_2', 'EXPECT_TEXT_3']) {
if (process.env[key]) {
expectContains(body, process.env[key], 'body');
}
}

console.log(`ok ${pagePath}`);
await browser.close();
}

main().catch((error) => {
console.error(error.stack || String(error));
process.exit(1);
});
8 changes: 8 additions & 0 deletions tests/e2e/playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "plugin-syslog-e2e",
"private": true,
"version": "1.0.0",
"devDependencies": {
"playwright": "1.52.0"
}
}
142 changes: 142 additions & 0 deletions tests/e2e/playwright/run-e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
set -euo pipefail

PW_IMAGE="mcr.microsoft.com/playwright:v1.52.0-jammy"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
PW_PREFIX=(
docker run --rm
--network cacti-syslog-e2e_default
--ipc=host
-v "${ROOT_DIR}/tests/e2e/playwright:/work"
-w /work
)

pass_count=0

db_scalar() {
docker exec cacti_db mariadb -N -B -ucacti -pcacti cacti -e "$1"
}

db_exec() {
docker exec cacti_db mariadb -ucacti -pcacti cacti -e "$1" >/dev/null
}

browser_check() {
local page_path="$1"
local expect_title="$2"
local expect_text="$3"
local expect_text_2="${4:-}"
local expect_text_3="${5:-}"

"${PW_PREFIX[@]}" \
-e "PAGE_PATH=${page_path}" \
-e "EXPECT_TITLE=${expect_title}" \
-e "EXPECT_TEXT=${expect_text}" \
-e "EXPECT_TEXT_2=${expect_text_2}" \
-e "EXPECT_TEXT_3=${expect_text_3}" \
"$PW_IMAGE" \
node browser-check.js >/dev/null
}

run_test() {
local name="$1"
shift
echo "TEST ${name}"
"$@"
pass_count=$((pass_count + 1))
echo "PASS ${name}"
}

assert_equals() {
local actual="$1"
local expected="$2"
local label="$3"

if [[ "$actual" != "$expected" ]]; then
echo "FAIL ${label}: expected '${expected}', got '${actual}'" >&2
exit 1
fi
}

normalize_admin() {
docker exec cacti_web php -r 'require "include/global.php"; $hash = password_hash("Admin123!", PASSWORD_BCRYPT); db_execute_prepared("UPDATE user_auth SET password = ?, must_change_password = \"\", password_change = \"\" WHERE username = \"admin\"", [$hash]);' >/dev/null
}

reset_syslog_data() {
db_exec "TRUNCATE syslog_incoming; TRUNCATE syslog; TRUNCATE syslog_removed; TRUNCATE syslog_statistics; TRUNCATE syslog_hosts; TRUNCATE syslog_programs; TRUNCATE syslog_host_facilities; DELETE FROM syslog_remove;"
}

seed_incoming() {
local host="$1"
local program="$2"
local message="$3"
db_exec "INSERT INTO syslog_incoming (facility_id, priority_id, program, logtime, host, message) VALUES (1, 6, '${program}', NOW(), '${host}', '${message}')"
}

run_processor() {
docker exec cacti_web php /var/www/html/cacti/plugins/syslog/syslog_process.php --debug >/dev/null
}

test_login_console() {
browser_check "index.php" "Console" "Main Console" "Logged in as admin"
}

test_syslog_empty_page() {
reset_syslog_data
browser_check "plugins/syslog/syslog.php" "Console > Syslog" "No Syslog Messages" "Unprocessed Messages: 0"
}

test_alerts_page() {
browser_check "plugins/syslog/syslog_alerts.php" "Console > Syslog Alerts" "No Syslog Alerts Defined"
}

test_removal_page() {
browser_check "plugins/syslog/syslog_removal.php" "Console > Syslog Removal" "No Syslog Removal Rules Defined"
}

test_reports_page() {
browser_check "plugins/syslog/syslog_reports.php" "Console > Syslog Reports" "No Syslog Reports Defined"
}

test_plain_ingest_to_main() {
seed_incoming "e2e-host-1" "e2e-prog-1" "E2E-TOKEN-ONE plain ingest"
run_processor
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_incoming")" "0" "incoming queue should be drained"
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog WHERE message = 'E2E-TOKEN-ONE plain ingest'")" "1" "plain message should land in syslog"
}

test_plain_ingest_normalization() {
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_hosts WHERE host = 'e2e-host-1'")" "1" "host should be normalized"
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_programs WHERE program = 'e2e-prog-1'")" "1" "program should be normalized"
}

test_plain_ingest_visible_in_ui() {
browser_check "plugins/syslog/syslog.php?rfilter=E2E-TOKEN-ONE" "Console > Syslog" "E2E-TOKEN-ONE" "e2e-host-1" "e2e-prog-1"
}

test_second_ingest_accumulates() {
seed_incoming "e2e-host-2" "e2e-prog-2" "E2E-TOKEN-TWO second ingest"
run_processor
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog")" "2" "two processed records should exist"
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_hosts WHERE host = 'e2e-host-2'")" "1" "second host should be normalized"
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_programs WHERE program = 'e2e-prog-2'")" "1" "second program should be normalized"
}

test_second_ingest_visible_in_ui() {
browser_check "plugins/syslog/syslog.php?rfilter=E2E-TOKEN-TWO" "Console > Syslog" "E2E-TOKEN-TWO" "e2e-host-2" "e2e-prog-2"
}

normalize_admin

run_test "1 login console" test_login_console
run_test "2 syslog empty page" test_syslog_empty_page
run_test "3 alerts page" test_alerts_page
run_test "4 removal page" test_removal_page
run_test "5 reports page" test_reports_page
run_test "6 plain ingest to main" test_plain_ingest_to_main
run_test "7 plain ingest normalization" test_plain_ingest_normalization
run_test "8 plain ingest visible in ui" test_plain_ingest_visible_in_ui
run_test "9 second ingest accumulates" test_second_ingest_accumulates
run_test "10 second ingest visible in ui" test_second_ingest_visible_in_ui

echo "ALL PASS ${pass_count}"
98 changes: 98 additions & 0 deletions tests/e2e/run-orb-docker-e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CACTI_REPO="${CACTI_REPO:-$(cd "${ROOT_DIR}/../cacti" && pwd)}"
TEMP_DIR="${TEMP_DIR:-/tmp/cacti-syslog-e2e}"
PW_IMAGE="${PW_IMAGE:-mcr.microsoft.com/playwright:v1.52.0-jammy}"

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm -rf "${TEMP_DIR}" will recursively delete whatever path a caller puts in TEMP_DIR. Since this script allows overriding TEMP_DIR via environment, it’s easy to accidentally point it at an unsafe location (e.g., / or a non-temp directory). Add a sanity check before deleting (e.g., require it to be non-empty and under /tmp, or use mktemp -d and refuse to run if TEMP_DIR is not within a safe prefix).

Suggested change
case "${TEMP_DIR}" in
""|"/"|"/tmp"|"/tmp/"|"."|"..")
echo "Refusing to use unsafe TEMP_DIR: ${TEMP_DIR}" >&2
exit 1
;;
/tmp/*)
;;
*)
echo "Refusing to use TEMP_DIR outside /tmp: ${TEMP_DIR}" >&2
exit 1
;;
esac

Copilot uses AI. Check for mistakes.
rm -rf "${TEMP_DIR}"
mkdir -p "${TEMP_DIR}/plugins/syslog"

rsync -a --delete "${CACTI_REPO}/" "${TEMP_DIR}/"
rsync -a --delete "${ROOT_DIR}/" "${TEMP_DIR}/plugins/syslog/"

cat > "${TEMP_DIR}/.env" <<'EOF'
WEB_PORT=18080
PHP_MEMORY_LIMIT=512M
DB_ROOT_PASSWORD=root
DB_NAME=cacti
DB_USER=cacti
DB_PASSWORD=cacti
DB_PORT=13306
DB_MAX_CONNECTIONS=200
DB_BUFFER_POOL_SIZE=512M
TIMEZONE=UTC
EOF

cat > "${TEMP_DIR}/plugins/syslog/config.php" <<'EOF'
<?php

global $config, $database_type, $database_default, $database_hostname;
global $database_username, $database_password, $database_port, $database_retries;
global $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca;

$use_cacti_db = true;

if (!$use_cacti_db) {
$syslogdb_type = 'mysql';
$syslogdb_default = 'syslog';
$syslogdb_hostname = 'localhost';
$syslogdb_username = 'cactiuser';
$syslogdb_password = 'cactiuser';
$syslogdb_port = 3306;
$syslogdb_retries = 5;
$syslogdb_ssl = false;
$syslogdb_ssl_key = '';
$syslogdb_ssl_cert = '';
$syslogdb_ssl_ca = '';
} else {
$syslogdb_type = $database_type;
$syslogdb_default = $database_default;
$syslogdb_hostname = $database_hostname;
$syslogdb_username = $database_username;
$syslogdb_password = $database_password;
$syslogdb_port = $database_port;
$syslogdb_retries = $database_retries;
$syslogdb_ssl = $database_ssl;
$syslogdb_ssl_key = $database_ssl_key;
$syslogdb_ssl_cert = $database_ssl_cert;
$syslogdb_ssl_ca = $database_ssl_ca;
}

$syslog_install_options['upgrade_type'] = 'truncate';
$syslog_install_options['engine'] = 'innodb';
$syslog_install_options['db_type'] = 'trad';
$syslog_install_options['days'] = '30';
$syslog_install_options['mode'] = 'install';
$syslog_install_options['id'] = 'syslog';

$syslog_incoming_config['priorityField'] = 'priority_id';
$syslog_incoming_config['facilityField'] = 'facility_id';
$syslog_incoming_config['programField'] = 'program';
$syslog_incoming_config['timeField'] = 'logtime';
$syslog_incoming_config['hostField'] = 'host';
$syslog_incoming_config['textField'] = 'message';
$syslog_incoming_config['id'] = 'seq';
EOF

docker compose -f "${TEMP_DIR}/docker-compose.yml" down -v >/dev/null 2>&1 || true
docker compose -f "${TEMP_DIR}/docker-compose.yml" up -d --build

docker exec cacti_web ln -sf /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
docker exec cacti_web sh -lc 'mkdir -p /var/www/html/cacti/cache/boost /var/www/html/cacti/cache/mibcache /var/www/html/cacti/cache/realtime /var/www/html/cacti/cache/spikekill && chown -R www-data:www-data /var/www/html/cacti/cache'
docker exec cacti_web sh -lc 'apt-get update >/dev/null && apt-get install -y --no-install-recommends fping >/dev/null'
docker exec cacti_web sh -lc 'touch /var/www/html/cacti/log/cacti.log && chown www-data:www-data /var/www/html/cacti/log/cacti.log'

docker exec cacti_web php cli/install_cacti.php --accept-eula --install --force
docker exec cacti_web php cli/plugin_manage.php --plugin=syslog --install --enable --allperms

docker run --rm \
--network cacti-syslog-e2e_default \
--ipc=host \
-v "${ROOT_DIR}/tests/e2e/playwright:/work" \
-w /work \
"${PW_IMAGE}" \
npm install

"${ROOT_DIR}/tests/e2e/playwright/run-e2e.sh"
Loading
Loading