Skip to content

Commit a93dfe6

Browse files
test: add Pest v1 security test infrastructure
1 parent a626774 commit a93dfe6

7 files changed

Lines changed: 787 additions & 0 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const { chromium } = require('playwright');
2+
3+
function expectContains(haystack, needle, label) {
4+
if (!haystack.includes(needle)) {
5+
throw new Error(`Expected ${label} to contain: ${needle}`);
6+
}
7+
}
8+
9+
async function login(page) {
10+
await page.goto('http://cacti_web/cacti/', { waitUntil: 'domcontentloaded' });
11+
await page.locator('#login_username').fill('admin');
12+
await page.locator('#login_password').fill('Admin123!');
13+
await Promise.all([
14+
page.waitForURL(/\/cacti\/index\.php/, { timeout: 30000 }),
15+
page.locator('form#auth').evaluate((form) => form.submit())
16+
]);
17+
}
18+
19+
async function main() {
20+
const browser = await chromium.launch({
21+
headless: true,
22+
args: ['--no-sandbox', '--disable-dev-shm-usage']
23+
});
24+
const page = await browser.newPage();
25+
26+
await login(page);
27+
28+
const pagePath = process.env.PAGE_PATH || 'index.php';
29+
await page.goto(`http://cacti_web/cacti/${pagePath}`, { waitUntil: 'domcontentloaded' });
30+
31+
const title = await page.title();
32+
const body = await page.locator('body').innerText();
33+
34+
if (process.env.EXPECT_TITLE) {
35+
expectContains(title, process.env.EXPECT_TITLE, 'title');
36+
}
37+
38+
for (const key of ['EXPECT_TEXT', 'EXPECT_TEXT_2', 'EXPECT_TEXT_3']) {
39+
if (process.env[key]) {
40+
expectContains(body, process.env[key], 'body');
41+
}
42+
}
43+
44+
console.log(`ok ${pagePath}`);
45+
await browser.close();
46+
}
47+
48+
main().catch((error) => {
49+
console.error(error.stack || String(error));
50+
process.exit(1);
51+
});

tests/e2e/playwright/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "plugin-syslog-e2e",
3+
"private": true,
4+
"version": "1.0.0",
5+
"devDependencies": {
6+
"playwright": "1.52.0"
7+
}
8+
}

tests/e2e/playwright/run-e2e.sh

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PW_IMAGE="mcr.microsoft.com/playwright:v1.52.0-jammy"
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
6+
PW_PREFIX=(
7+
docker run --rm
8+
--network cacti-syslog-e2e_default
9+
--ipc=host
10+
-v "${ROOT_DIR}/tests/e2e/playwright:/work"
11+
-w /work
12+
)
13+
14+
pass_count=0
15+
16+
db_scalar() {
17+
docker exec cacti_db mariadb -N -B -ucacti -pcacti cacti -e "$1"
18+
}
19+
20+
db_exec() {
21+
docker exec cacti_db mariadb -ucacti -pcacti cacti -e "$1" >/dev/null
22+
}
23+
24+
browser_check() {
25+
local page_path="$1"
26+
local expect_title="$2"
27+
local expect_text="$3"
28+
local expect_text_2="${4:-}"
29+
local expect_text_3="${5:-}"
30+
31+
"${PW_PREFIX[@]}" \
32+
-e "PAGE_PATH=${page_path}" \
33+
-e "EXPECT_TITLE=${expect_title}" \
34+
-e "EXPECT_TEXT=${expect_text}" \
35+
-e "EXPECT_TEXT_2=${expect_text_2}" \
36+
-e "EXPECT_TEXT_3=${expect_text_3}" \
37+
"$PW_IMAGE" \
38+
node browser-check.js >/dev/null
39+
}
40+
41+
run_test() {
42+
local name="$1"
43+
shift
44+
echo "TEST ${name}"
45+
"$@"
46+
pass_count=$((pass_count + 1))
47+
echo "PASS ${name}"
48+
}
49+
50+
assert_equals() {
51+
local actual="$1"
52+
local expected="$2"
53+
local label="$3"
54+
55+
if [[ "$actual" != "$expected" ]]; then
56+
echo "FAIL ${label}: expected '${expected}', got '${actual}'" >&2
57+
exit 1
58+
fi
59+
}
60+
61+
normalize_admin() {
62+
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
63+
}
64+
65+
reset_syslog_data() {
66+
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;"
67+
}
68+
69+
seed_incoming() {
70+
local host="$1"
71+
local program="$2"
72+
local message="$3"
73+
db_exec "INSERT INTO syslog_incoming (facility_id, priority_id, program, logtime, host, message) VALUES (1, 6, '${program}', NOW(), '${host}', '${message}')"
74+
}
75+
76+
run_processor() {
77+
docker exec cacti_web php /var/www/html/cacti/plugins/syslog/syslog_process.php --debug >/dev/null
78+
}
79+
80+
test_login_console() {
81+
browser_check "index.php" "Console" "Main Console" "Logged in as admin"
82+
}
83+
84+
test_syslog_empty_page() {
85+
reset_syslog_data
86+
browser_check "plugins/syslog/syslog.php" "Console > Syslog" "No Syslog Messages" "Unprocessed Messages: 0"
87+
}
88+
89+
test_alerts_page() {
90+
browser_check "plugins/syslog/syslog_alerts.php" "Console > Syslog Alerts" "No Syslog Alerts Defined"
91+
}
92+
93+
test_removal_page() {
94+
browser_check "plugins/syslog/syslog_removal.php" "Console > Syslog Removal" "No Syslog Removal Rules Defined"
95+
}
96+
97+
test_reports_page() {
98+
browser_check "plugins/syslog/syslog_reports.php" "Console > Syslog Reports" "No Syslog Reports Defined"
99+
}
100+
101+
test_plain_ingest_to_main() {
102+
seed_incoming "e2e-host-1" "e2e-prog-1" "E2E-TOKEN-ONE plain ingest"
103+
run_processor
104+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_incoming")" "0" "incoming queue should be drained"
105+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog WHERE message = 'E2E-TOKEN-ONE plain ingest'")" "1" "plain message should land in syslog"
106+
}
107+
108+
test_plain_ingest_normalization() {
109+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_hosts WHERE host = 'e2e-host-1'")" "1" "host should be normalized"
110+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_programs WHERE program = 'e2e-prog-1'")" "1" "program should be normalized"
111+
}
112+
113+
test_plain_ingest_visible_in_ui() {
114+
browser_check "plugins/syslog/syslog.php?rfilter=E2E-TOKEN-ONE" "Console > Syslog" "E2E-TOKEN-ONE" "e2e-host-1" "e2e-prog-1"
115+
}
116+
117+
test_second_ingest_accumulates() {
118+
seed_incoming "e2e-host-2" "e2e-prog-2" "E2E-TOKEN-TWO second ingest"
119+
run_processor
120+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog")" "2" "two processed records should exist"
121+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_hosts WHERE host = 'e2e-host-2'")" "1" "second host should be normalized"
122+
assert_equals "$(db_scalar "SELECT COUNT(*) FROM syslog_programs WHERE program = 'e2e-prog-2'")" "1" "second program should be normalized"
123+
}
124+
125+
test_second_ingest_visible_in_ui() {
126+
browser_check "plugins/syslog/syslog.php?rfilter=E2E-TOKEN-TWO" "Console > Syslog" "E2E-TOKEN-TWO" "e2e-host-2" "e2e-prog-2"
127+
}
128+
129+
normalize_admin
130+
131+
run_test "1 login console" test_login_console
132+
run_test "2 syslog empty page" test_syslog_empty_page
133+
run_test "3 alerts page" test_alerts_page
134+
run_test "4 removal page" test_removal_page
135+
run_test "5 reports page" test_reports_page
136+
run_test "6 plain ingest to main" test_plain_ingest_to_main
137+
run_test "7 plain ingest normalization" test_plain_ingest_normalization
138+
run_test "8 plain ingest visible in ui" test_plain_ingest_visible_in_ui
139+
run_test "9 second ingest accumulates" test_second_ingest_accumulates
140+
run_test "10 second ingest visible in ui" test_second_ingest_visible_in_ui
141+
142+
echo "ALL PASS ${pass_count}"

tests/e2e/run-orb-docker-e2e.sh

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
CACTI_REPO="${CACTI_REPO:-$(cd "${ROOT_DIR}/../cacti" && pwd)}"
6+
TEMP_DIR="${TEMP_DIR:-/tmp/cacti-syslog-e2e}"
7+
PW_IMAGE="${PW_IMAGE:-mcr.microsoft.com/playwright:v1.52.0-jammy}"
8+
9+
rm -rf "${TEMP_DIR}"
10+
mkdir -p "${TEMP_DIR}/plugins/syslog"
11+
12+
rsync -a --delete "${CACTI_REPO}/" "${TEMP_DIR}/"
13+
rsync -a --delete "${ROOT_DIR}/" "${TEMP_DIR}/plugins/syslog/"
14+
15+
cat > "${TEMP_DIR}/.env" <<'EOF'
16+
WEB_PORT=18080
17+
PHP_MEMORY_LIMIT=512M
18+
DB_ROOT_PASSWORD=root
19+
DB_NAME=cacti
20+
DB_USER=cacti
21+
DB_PASSWORD=cacti
22+
DB_PORT=13306
23+
DB_MAX_CONNECTIONS=200
24+
DB_BUFFER_POOL_SIZE=512M
25+
TIMEZONE=UTC
26+
EOF
27+
28+
cat > "${TEMP_DIR}/plugins/syslog/config.php" <<'EOF'
29+
<?php
30+
31+
global $config, $database_type, $database_default, $database_hostname;
32+
global $database_username, $database_password, $database_port, $database_retries;
33+
global $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca;
34+
35+
$use_cacti_db = true;
36+
37+
if (!$use_cacti_db) {
38+
$syslogdb_type = 'mysql';
39+
$syslogdb_default = 'syslog';
40+
$syslogdb_hostname = 'localhost';
41+
$syslogdb_username = 'cactiuser';
42+
$syslogdb_password = 'cactiuser';
43+
$syslogdb_port = 3306;
44+
$syslogdb_retries = 5;
45+
$syslogdb_ssl = false;
46+
$syslogdb_ssl_key = '';
47+
$syslogdb_ssl_cert = '';
48+
$syslogdb_ssl_ca = '';
49+
} else {
50+
$syslogdb_type = $database_type;
51+
$syslogdb_default = $database_default;
52+
$syslogdb_hostname = $database_hostname;
53+
$syslogdb_username = $database_username;
54+
$syslogdb_password = $database_password;
55+
$syslogdb_port = $database_port;
56+
$syslogdb_retries = $database_retries;
57+
$syslogdb_ssl = $database_ssl;
58+
$syslogdb_ssl_key = $database_ssl_key;
59+
$syslogdb_ssl_cert = $database_ssl_cert;
60+
$syslogdb_ssl_ca = $database_ssl_ca;
61+
}
62+
63+
$syslog_install_options['upgrade_type'] = 'truncate';
64+
$syslog_install_options['engine'] = 'innodb';
65+
$syslog_install_options['db_type'] = 'trad';
66+
$syslog_install_options['days'] = '30';
67+
$syslog_install_options['mode'] = 'install';
68+
$syslog_install_options['id'] = 'syslog';
69+
70+
$syslog_incoming_config['priorityField'] = 'priority_id';
71+
$syslog_incoming_config['facilityField'] = 'facility_id';
72+
$syslog_incoming_config['programField'] = 'program';
73+
$syslog_incoming_config['timeField'] = 'logtime';
74+
$syslog_incoming_config['hostField'] = 'host';
75+
$syslog_incoming_config['textField'] = 'message';
76+
$syslog_incoming_config['id'] = 'seq';
77+
EOF
78+
79+
docker compose -f "${TEMP_DIR}/docker-compose.yml" down -v >/dev/null 2>&1 || true
80+
docker compose -f "${TEMP_DIR}/docker-compose.yml" up -d --build
81+
82+
docker exec cacti_web ln -sf /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
83+
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'
84+
docker exec cacti_web sh -lc 'apt-get update >/dev/null && apt-get install -y --no-install-recommends fping >/dev/null'
85+
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'
86+
87+
docker exec cacti_web php cli/install_cacti.php --accept-eula --install --force
88+
docker exec cacti_web php cli/plugin_manage.php --plugin=syslog --install --enable --allperms
89+
90+
docker run --rm \
91+
--network cacti-syslog-e2e_default \
92+
--ipc=host \
93+
-v "${ROOT_DIR}/tests/e2e/playwright:/work" \
94+
-w /work \
95+
"${PW_IMAGE}" \
96+
npm install
97+
98+
"${ROOT_DIR}/tests/e2e/playwright/run-e2e.sh"

0 commit comments

Comments
 (0)