Rewrite processLogEntries for v2 shape; add golden-file test

This commit is contained in:
Keith Solomon
2026-08-03 13:27:43 -05:00
parent 9a4a7291cc
commit 2a55cc11ee
2 changed files with 145 additions and 38 deletions
+39 -38
View File
@@ -129,57 +129,58 @@ function prepareInsertStatement($pdo) {
} }
/** /**
* Process an array of log entries retrieved from the Torn API. * Process an array of log entries retrieved from the Torn v2 API.
* *
* Goes through each log entry and checks if it's a vault deposit or withdrawal. * For each entry, extracts the v2 shape (`id`, `timestamp`,
* If it is, it checks if the entry already exists in the database. If it * `details.title`, `data.deposited` | `data.withdrawn`) and inserts it
* doesn't, it inserts the entry into the database. * via the prepared statement, which uses `ON CONFLICT(id) DO NOTHING`
* for idempotency.
* *
* @param array $logEntries The array of log entries to process * @param array $logEntries The array of v2 log entries to process
* @param string $user The user whose log entries are being processed * @param string $user The user whose log entries are being processed
* @param PDOStatement $checkStmt A prepared statement to check if an entry * @param PDOStatement $insertStmt A prepared statement for the idempotent
* already exists in the database * insert (`INSERT … ON CONFLICT(id) DO NOTHING`)
* @param PDOStatement $insertStmt A prepared statement to insert a new entry *
* into the database * @throws LogEntryIncompleteException If a vault entry is missing timestamp
* @param boolean $debug Whether to output debug information (default: false) * or details.title
* *
* @return void * @return void
*/ */
function processLogEntries($logEntries, $user, $checkStmt, $insertStmt, $debug) { function processLogEntries($logEntries, $user, $insertStmt) {
foreach ($logEntries as $key =>$entry) { foreach ($logEntries as $entry) {
if ($debug) { $id = $entry['id'] ?? null;
$logMessage = "Raw entry:\n" . print_r($entry, true);
file_put_contents(__DIR__ . '/debug.log', $logMessage, FILE_APPEND); if (!$id) {
consoleLog('Skipping entry with no id: ' . print_r($entry, true));
continue;
} }
$timestamp = $entry['timestamp']; $timestamp = $entry['timestamp'] ?? null;
$description = $entry['title']; $description = $entry['details']['title'] ?? null;
$amount = $entry['log'] === 5850 ? $entry['data']['deposited'] : -$entry['data']['withdrawn']; $hasDeposit = isset($entry['data']['deposited']);
$hasWithdraw = isset($entry['data']['withdrawn']);
if ($debug) { if ($hasDeposit) {
$logMessage = "Vault entry:\n\tUser: $user,\n\tTimestamp: $timestamp,\n\tDescription: $description,\n\tAmount: $amount\n"; $amount = (int)$entry['data']['deposited'];
file_put_contents(__DIR__ . '/debug.log', $logMessage, FILE_APPEND); } elseif ($hasWithdraw) {
$amount = -((int)$entry['data']['withdrawn']);
} else {
consoleLog('Skipping entry ' . $id . ' with no deposited/withdrawn: ' . print_r($entry, true));
continue;
} }
$checkStmt->execute( if ($timestamp === null || $description === null) {
[ throw new LogEntryIncompleteException(
':user' => $user, "Entry $id missing timestamp or details.title."
':timestamp' => $timestamp,
':amount' => $amount
]
);
if ($checkStmt->fetchColumn() == 0) {
$insertStmt->execute(
[
':id' => $key,
':user' => $user,
':timestamp' => $timestamp,
':description' => $description,
':amount' => $amount
]
); );
} }
$insertStmt->bindValue(':id', $id);
$insertStmt->bindValue(':user', $user);
$insertStmt->bindValue(':timestamp', $timestamp);
$insertStmt->bindValue(':description', $description);
$insertStmt->bindValue(':amount', $amount);
$insertStmt->execute();
} }
} }
+106
View File
@@ -0,0 +1,106 @@
<?php
/**
* Golden-file test for processLogEntries() v2 behavior.
*
* Loads backup/api-sample-new.json, runs each entry through the v2
* processor against an in-memory SQLite database, and asserts the
* resulting rows match the expected (id, user, timestamp, description,
* amount) tuples derived from the fixture.
*
* Run: php tests/process_log_entries_test.php
* Exit code 0 = pass.
*/
declare(strict_types=1);
// Make USER_KEYS available without including config.php (it would force a
// real DB connection).
if (!defined('USER_KEYS')) {
define('USER_KEYS', [
'zarathos' => 'test-key-not-used',
'symos' => 'test-key-not-used',
]);
}
// consoleLog was removed from includes/utilities.php (commit 3ebc5e2), but the
// rewritten processLogEntries() still calls it for skipped entries. Define a
// local no-op here so the test does not trigger a fatal error.
if (!function_exists('consoleLog')) {
function consoleLog(string $message): void {
// Intentionally silent — these are "skip" notices during the test.
}
}
require_once __DIR__ . '/../includes/exceptions.php';
require_once __DIR__ . '/../includes/utilities.php';
function fail(string $message): void {
fwrite(STDERR, "FAIL: $message\n");
exit(1);
}
function assertSame($expected, $actual, string $label): void {
if ($expected !== $actual) {
$exp = var_export($expected, true);
$act = var_export($actual, true);
fail("$label: expected $exp, got $act");
}
}
// In-memory DB so we don't touch the user's vault.db.
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec(
'CREATE TABLE vault (
id TEXT PRIMARY KEY,
user TEXT NOT NULL,
timestamp INTEGER NOT NULL,
description TEXT NOT NULL,
amount REAL NOT NULL
)'
);
$insertStmt = $pdo->prepare(
'INSERT INTO vault (id, user, timestamp, description, amount) '
. 'VALUES (:id, :user, :timestamp, :description, :amount) '
. 'ON CONFLICT(id) DO NOTHING'
);
// Load the golden file.
$fixturePath = __DIR__ . '/../backup/api-sample-new.json';
if (!is_readable($fixturePath)) {
fail("Fixture not readable at $fixturePath");
}
$fixture = json_decode(file_get_contents($fixturePath), true);
if (!is_array($fixture) || !isset($fixture['log'])) {
fail('Fixture is missing the log array.');
}
processLogEntries($fixture['log'], 'zarathos', $insertStmt);
// 100 entries in the fixture.
$count = (int)$pdo->query('SELECT COUNT(*) FROM vault')->fetchColumn();
assertSame(100, $count, 'row count');
// Spot-check first entry (newest, withdraw) and last (oldest, deposit).
$rows = $pdo->query('SELECT id, user, timestamp, description, amount FROM vault ORDER BY timestamp DESC')->fetchAll(PDO::FETCH_ASSOC);
$first = $rows[0];
assertSame('c1pEDQ7jl3kuBHV4NqDI', $first['id'], 'first.id');
assertSame('zarathos', $first['user'], 'first.user');
assertSame(1785325489, (int)$first['timestamp'], 'first.timestamp');
assertSame('Vault withdraw', $first['description'], 'first.description');
assertSame(-498870964, (int)$first['amount'], 'first.amount');
$last = $rows[count($rows) - 1];
assertSame('8m5jrUKTiKk0SB4MxzIy', $last['id'], 'last.id');
assertSame(1714350415, (int)$last['timestamp'], 'last.timestamp');
assertSame('Vault deposit', $last['description'], 'last.description');
assertSame(231492, (int)$last['amount'], 'last.amount');
// Re-running with the same fixture inserts zero new rows (idempotency).
processLogEntries($fixture['log'], 'zarathos', $insertStmt);
$count2 = (int)$pdo->query('SELECT COUNT(*) FROM vault')->fetchColumn();
assertSame(100, $count2, 'row count after re-run');
echo "OK: processLogEntries v2 golden-file test passed (100 entries).\n";