From 2a55cc11ee4d3cf958dbc5c8f67a40c28460ebdd Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Mon, 3 Aug 2026 13:27:43 -0500 Subject: [PATCH] Rewrite processLogEntries for v2 shape; add golden-file test --- includes/utilities.php | 77 ++++++++++----------- tests/process_log_entries_test.php | 106 +++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 38 deletions(-) create mode 100644 tests/process_log_entries_test.php diff --git a/includes/utilities.php b/includes/utilities.php index 320521d..d2d2edc 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -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. - * If it is, it checks if the entry already exists in the database. If it - * doesn't, it inserts the entry into the database. + * For each entry, extracts the v2 shape (`id`, `timestamp`, + * `details.title`, `data.deposited` | `data.withdrawn`) and inserts it + * 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 PDOStatement $checkStmt A prepared statement to check if an entry - * already exists in the database - * @param PDOStatement $insertStmt A prepared statement to insert a new entry - * into the database - * @param boolean $debug Whether to output debug information (default: false) + * @param PDOStatement $insertStmt A prepared statement for the idempotent + * insert (`INSERT … ON CONFLICT(id) DO NOTHING`) + * + * @throws LogEntryIncompleteException If a vault entry is missing timestamp + * or details.title * * @return void */ -function processLogEntries($logEntries, $user, $checkStmt, $insertStmt, $debug) { - foreach ($logEntries as $key =>$entry) { - if ($debug) { - $logMessage = "Raw entry:\n" . print_r($entry, true); - file_put_contents(__DIR__ . '/debug.log', $logMessage, FILE_APPEND); +function processLogEntries($logEntries, $user, $insertStmt) { + foreach ($logEntries as $entry) { + $id = $entry['id'] ?? null; + + if (!$id) { + consoleLog('Skipping entry with no id: ' . print_r($entry, true)); + continue; } - $timestamp = $entry['timestamp']; - $description = $entry['title']; - $amount = $entry['log'] === 5850 ? $entry['data']['deposited'] : -$entry['data']['withdrawn']; + $timestamp = $entry['timestamp'] ?? null; + $description = $entry['details']['title'] ?? null; + $hasDeposit = isset($entry['data']['deposited']); + $hasWithdraw = isset($entry['data']['withdrawn']); - if ($debug) { - $logMessage = "Vault entry:\n\tUser: $user,\n\tTimestamp: $timestamp,\n\tDescription: $description,\n\tAmount: $amount\n"; - file_put_contents(__DIR__ . '/debug.log', $logMessage, FILE_APPEND); + if ($hasDeposit) { + $amount = (int)$entry['data']['deposited']; + } elseif ($hasWithdraw) { + $amount = -((int)$entry['data']['withdrawn']); + } else { + consoleLog('Skipping entry ' . $id . ' with no deposited/withdrawn: ' . print_r($entry, true)); + continue; } - $checkStmt->execute( - [ - ':user' => $user, - ':timestamp' => $timestamp, - ':amount' => $amount - ] - ); - - if ($checkStmt->fetchColumn() == 0) { - $insertStmt->execute( - [ - ':id' => $key, - ':user' => $user, - ':timestamp' => $timestamp, - ':description' => $description, - ':amount' => $amount - ] + if ($timestamp === null || $description === null) { + throw new LogEntryIncompleteException( + "Entry $id missing timestamp or details.title." ); } + + $insertStmt->bindValue(':id', $id); + $insertStmt->bindValue(':user', $user); + $insertStmt->bindValue(':timestamp', $timestamp); + $insertStmt->bindValue(':description', $description); + $insertStmt->bindValue(':amount', $amount); + $insertStmt->execute(); } } diff --git a/tests/process_log_entries_test.php b/tests/process_log_entries_test.php new file mode 100644 index 0000000..1c8acf6 --- /dev/null +++ b/tests/process_log_entries_test.php @@ -0,0 +1,106 @@ + '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";