Skip to content
Merged
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
91 changes: 91 additions & 0 deletions web/tests/e2e/fixtures/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const DELETE_SERVER_INSIDE_CONTAINER =
'/var/www/html/web/tests/e2e/scripts/delete-server-e2e.php';
const CLEAR_TEST_EMAIL_THROTTLE_INSIDE_CONTAINER =
'/var/www/html/web/tests/e2e/scripts/clear-test-email-throttle-e2e.php';
const SEED_SYSTEM_LOG_INSIDE_CONTAINER =
'/var/www/html/web/tests/e2e/scripts/seed-system-log-e2e.php';

/**
* Run the PHP shim that drives `Sbpp\Tests\Fixture` against
Expand Down Expand Up @@ -683,6 +685,95 @@ async function runAnnouncementsHelper(
});
}

/**
* Per-row shape consumed by `seedSystemLogE2e` (#1462). Mirrors the
* `:prefix_log` schema: `type` is the audit letter code
* (`m`=message, `w`=warning, `e`=error — matches `LogType`). Every
* field except `type` carries a string default — the spec just
* needs at least one row to exist so the `{if count($log_items) > 0}`
* gate in `page_admin_settings_logs.tpl` paints SOMETHING, and the
* mobile-card / desktop-table parity assertion can run.
*/
export interface SystemLogSeedRow {
type?: 'm' | 'w' | 'e';
title?: string;
message?: string;
function?: string;
query?: string;
host?: string;
}

/**
* Seed `:prefix_log` rows directly so the System Log sub-tab
* (`?p=admin&c=settings&section=logs`) actually paints log entries
* instead of the empty "No log entries." placeholder. The e2e DB's
* `:prefix_log` table is empty by default (`data.sql` doesn't ship
* audit rows + `Fixture::truncateAndReseed` truncates every table on
* reset) and there is no JSON action that emits audit rows directly
* — they're side effects of authenticated panel writes. Driving e.g.
* `Actions.BansAdd` to produce a row would couple the System Log
* spec to the bans-add audit message, so the direct INSERT is the
* narrow shape the spec needs.
*
* Caller responsibility: invoke after `truncateE2eDb()` so the
* seeded rows are the only ones the spec sees. Mirrors the
* `seedCommsRawE2e` shape (shell-out + stdin-JSON).
*
* Returns the list of inserted `lid` values (in insert order) so
* the spec can target a specific row via `[data-id="<lid>"]`.
*/
export async function seedSystemLogE2e(rows: SystemLogSeedRow[]): Promise<number[]> {
const inContainer = process.env.E2E_IN_CONTAINER === '1';
const cmd = inContainer ? 'php' : 'docker';
const cmdArgs = inContainer
? [SEED_SYSTEM_LOG_INSIDE_CONTAINER]
: ['compose', 'exec', '-T', 'web', 'php', SEED_SYSTEM_LOG_INSIDE_CONTAINER];

const child = execFile(cmd, cmdArgs, {
maxBuffer: 8 * 1024 * 1024,
cwd: inContainer ? undefined : process.cwd(),
});

let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString('utf8'); });
child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); });

child.stdin?.write(JSON.stringify(rows));
child.stdin?.end();

await new Promise<void>((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(
`seed-system-log-e2e.php exited ${code}\n`
+ `stdout:\n${stdout}\nstderr:\n${stderr}`,
));
});
});

const trimmed = stdout.trim();
if (trimmed === '') {
throw new Error(`seed-system-log-e2e.php: empty stdout\nstderr:\n${stderr}`);
}
try {
const parsed = JSON.parse(trimmed) as { lids: number[] };
if (!Array.isArray(parsed.lids)) {
throw new Error('missing lids key');
}
return parsed.lids;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(
`seed-system-log-e2e.php: malformed stdout (${msg})\nstdout:\n${trimmed}\nstderr:\n${stderr}`,
);
}
}

/**
* Clear the `system.test_email` rate-limit cache file (#1455).
*
Expand Down
149 changes: 149 additions & 0 deletions web/tests/e2e/scripts/seed-system-log-e2e.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php
// SourceBans++ (c) 2014-2026 SourceBans++ Dev Team
// Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0.
// See LICENSE.md for the full license text and THIRD-PARTY-NOTICES.txt for attributions.

/**
* E2E system-log seeder (#1462).
*
* The `:prefix_log` table is empty in the bare e2e seed — `data.sql`
* doesn't ship any audit rows, and `Fixture::truncateAndReseed`
* truncates every table on every reset. The mobile-card responsive
* spec needs at least one row in the table so the System Log sub-tab
* actually paints log content (the `{if count($log_items) > 0}`
* gate would otherwise render "No log entries." and the card surface
* wouldn't be exercised).
*
* Why a dedicated shim instead of driving the existing `Log::add`
* path:
* - `Log::add(LogType::Message, ...)` is the production write
* surface, but it's called from inside a panel request — it
* reads `$_SERVER['REMOTE_ADDR']`, `$userbank->GetAid()`, etc.
* A spec running outside a panel request can't seed via this
* path without a Playwright `page.goto(...)` that triggers an
* audited action as a side effect.
* - Driving e.g. `Actions.BansAdd` to produce an audit row would
* couple the System Log spec to the bans-add surface; a
* refactor of the bans-add audit message would break the
* unrelated System Log mobile spec for the wrong reason.
* - Direct INSERT keeps the spec independent of every other audit
* surface — same shape `seed-comms-e2e.php` uses for the
* `comms.type=3` rows that `Actions.CommsAdd` never emits.
*
* This shim is e2e-only: refuses any DB other than the e2e schema
* (default `sourcebans_e2e`) so a stray host-side invocation can't
* trash the dev DB. Mirrors `seed-comms-e2e.php`.
*
* Usage (inside the web container):
*
* echo '<JSON>' | php seed-system-log-e2e.php
*
* The JSON payload is a list of row dicts:
*
* [
* {"type": "m", "title": "E2E info", "message": "first row",
* "function": "e2e/spec", "query": "n/a", "host": "127.0.0.1"},
* {"type": "w", "title": "E2E warn", "message": "second row", ...},
* ...
* ]
*
* Type letters mirror `:prefix_log.type enum('m','w','e')`. Defaults:
* - `type`: "m"
* - `title`: "E2E seeded entry"
* - `message`: ""
* - `function`: "tests/e2e/seed"
* - `query`: ""
* - `host`: "127.0.0.1"
*
* `aid` is resolved live from the seeded admin row (mirrors
* `seed-comms-e2e.php`'s lookup shape so this script stays
* independent of `Sbpp\Tests\Fixture::adminAid`'s in-process cache).
*/

declare(strict_types=1);

if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "seed-system-log-e2e.php must run on the CLI.\n");
exit(2);
}

if (!getenv('DB_NAME')) {
putenv('DB_NAME=sourcebans_e2e');
$_ENV['DB_NAME'] = 'sourcebans_e2e';
$_SERVER['DB_NAME'] = 'sourcebans_e2e';
}

if (getenv('DB_NAME') === 'sourcebans_test' || getenv('DB_NAME') === 'sourcebans') {
fwrite(STDERR, "refusing to seed system log against DB_NAME=" . getenv('DB_NAME')
. ": this script must target a dedicated e2e DB (default sourcebans_e2e).\n");
exit(2);
}

require __DIR__ . '/../../bootstrap.php';

if (!isset($GLOBALS['PDO'])) {
$GLOBALS['PDO'] = new \Database(DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS, DB_PREFIX, DB_CHARSET);
}

$payload = stream_get_contents(STDIN);
if ($payload === false || trim($payload) === '') {
fwrite(STDERR, "seed-system-log-e2e.php: empty stdin payload.\n");
exit(2);
}

$rows = json_decode($payload, true);
if (!is_array($rows)) {
fwrite(STDERR, "seed-system-log-e2e.php: stdin is not a JSON array.\n");
exit(2);
}

$adminRow = $GLOBALS['PDO']->query(
"SELECT aid FROM `:prefix_admins` WHERE user = ? AND authid = ?"
)->single(['admin', 'STEAM_0:0:0']);
if (empty($adminRow['aid'])) {
fwrite(STDERR, "seed-system-log-e2e.php: cannot resolve admin row; was Fixture::install() run?\n");
exit(2);
}
$adminAid = (int)$adminRow['aid'];

$now = time();
$allowedTypes = ['m', 'w', 'e'];
$insertedLids = [];

foreach ($rows as $i => $row) {
if (!is_array($row)) {
fwrite(STDERR, "seed-system-log-e2e.php: row $i is not an object.\n");
exit(2);
}

$type = (string)($row['type'] ?? 'm');
$title = (string)($row['title'] ?? 'E2E seeded entry');
$message = (string)($row['message'] ?? '');
$function = (string)($row['function'] ?? 'tests/e2e/seed');
$query = (string)($row['query'] ?? '');
$host = (string)($row['host'] ?? '127.0.0.1');

if (!in_array($type, $allowedTypes, true)) {
fwrite(STDERR, "seed-system-log-e2e.php: row $i has unknown type '$type' (expected 'm', 'w', or 'e').\n");
exit(2);
}

$GLOBALS['PDO']->query(
"INSERT INTO `:prefix_log` "
. "(type, title, message, function, query, aid, host, created) "
. "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
)->execute([
$type,
$title,
$message,
$function,
$query,
$adminAid,
$host,
$now,
]);

$insertedLids[] = (int)$GLOBALS['PDO']->lastInsertId();
}

fwrite(STDOUT, json_encode(['lids' => $insertedLids]) . "\n");
Loading
Loading