Files
2meet-data-optimizer/tests/unit/MemberFields/PointsManagerTest.php
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

232 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Unit tests for WPDO_Points_Manager.
*
* Uses a custom $wpdb mock that tracks which SQL verbs were executed
* (BEGIN/START TRANSACTION, COMMIT, ROLLBACK) so we can verify
* transaction discipline without hitting a real database.
*/
class PointsManagerTest extends TestCase {
/** @var object Original $wpdb mock from bootstrap. */
private object $original_wpdb;
protected function setUp(): void {
if ( ! class_exists( 'WPDO_Points_Manager' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/integrations/class-tmdo-points-manager.php';
}
global $wpdb;
$this->original_wpdb = $wpdb;
// Install a controllable mock that also has insert_id.
$wpdb = $this->make_wpdb_mock();
}
protected function tearDown(): void {
global $wpdb;
$wpdb = $this->original_wpdb;
}
// ── credit() input validation ────────────────────────────────────────────
public function test_credit_rejects_zero_delta(): void {
$result = WPDO_Points_Manager::credit( 1, 0 );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'credit delta must be positive', $result['error'] );
}
public function test_credit_rejects_negative_delta(): void {
$result = WPDO_Points_Manager::credit( 1, -50 );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'credit delta must be positive', $result['error'] );
}
// ── debit() input validation ─────────────────────────────────────────────
public function test_debit_rejects_zero_delta(): void {
$result = WPDO_Points_Manager::debit( 1, 0 );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'debit delta must be positive', $result['error'] );
}
public function test_debit_rejects_negative_delta(): void {
$result = WPDO_Points_Manager::debit( 1, -10 );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'debit delta must be positive', $result['error'] );
}
// ── debit() insufficient balance ─────────────────────────────────────────
public function test_debit_fails_when_balance_zero_and_no_overdraft(): void {
// $wpdb->get_var returns null → balance = 0; debit 50 → new_balance = -50 → reject.
$result = WPDO_Points_Manager::debit( 42, 50, 'purchase' );
$this->assertFalse( $result['ok'] );
$this->assertSame( 'insufficient_balance', $result['error'] );
}
public function test_debit_rollback_called_on_insufficient_balance(): void {
global $wpdb;
WPDO_Points_Manager::debit( 42, 50 );
$sql_log = $wpdb->queries;
// Expect BEGIN and ROLLBACK but NOT COMMIT.
$this->assertContains( 'START TRANSACTION', $sql_log );
$this->assertContains( 'ROLLBACK', $sql_log );
$this->assertNotContains( 'COMMIT', $sql_log );
}
// ── debit() allow_overdraft ───────────────────────────────────────────────
public function test_debit_with_allow_overdraft_succeeds_below_zero(): void {
$result = WPDO_Points_Manager::debit( 1, 100, 'force', 0, '', true );
$this->assertTrue( $result['ok'] );
$this->assertSame( -100, $result['balance'] );
}
// ── credit() happy path ───────────────────────────────────────────────────
public function test_credit_returns_ok_and_new_balance(): void {
$result = WPDO_Points_Manager::credit( 7, 200, 'signup_bonus' );
$this->assertTrue( $result['ok'] );
$this->assertSame( 200, $result['balance'] );
$this->assertArrayHasKey( 'ledger_id', $result );
}
public function test_credit_records_begin_and_commit(): void {
global $wpdb;
WPDO_Points_Manager::credit( 7, 100, 'test' );
$sql_log = $wpdb->queries;
$this->assertContains( 'START TRANSACTION', $sql_log );
$this->assertContains( 'COMMIT', $sql_log );
$this->assertNotContains( 'ROLLBACK', $sql_log );
}
public function test_credit_truncates_long_reason(): void {
// Reasons over 60 chars must be silently truncated (not cause DB error).
$long_reason = str_repeat( 'x', 100 );
$result = WPDO_Points_Manager::credit( 5, 10, $long_reason );
$this->assertTrue( $result['ok'] );
}
// ── get_balance() ─────────────────────────────────────────────────────────
public function test_get_balance_returns_zero_for_unknown_user(): void {
// Mock $wpdb->get_var returns null → (int) null = 0.
$balance = WPDO_Points_Manager::get_balance( 9999 );
$this->assertSame( 0, $balance );
}
// ── get_ledger() ─────────────────────────────────────────────────────────
public function test_get_ledger_returns_empty_array_when_no_rows(): void {
$ledger = WPDO_Points_Manager::get_ledger( 9999 );
$this->assertSame( array(), $ledger );
}
public function test_get_ledger_clamps_limit_between_1_and_500(): void {
// Just confirm no exception on extreme inputs.
WPDO_Points_Manager::get_ledger( 1, -5 );
WPDO_Points_Manager::get_ledger( 1, 9999 );
$this->assertTrue( true );
}
// ── helper ───────────────────────────────────────────────────────────────
/**
* Build a $wpdb mock that:
* - Records every SQL statement to ->queries[]
* - Returns null for SELECT…FOR UPDATE (simulating empty DB / no row)
* - After a successful UPSERT, returns the inserted delta for re-read SELECTs
* - Returns empty array for get_results
* - Returns 1 for query/insert
* - Has insert_id = 99
*/
private function make_wpdb_mock(): object {
return new class {
public string $prefix = 'wp_';
public string $postmeta = 'wp_postmeta';
public string $posts = 'wp_posts';
public string $options = 'wp_options';
public string $usermeta = 'wp_usermeta';
public string $users = 'wp_users';
public array $queries = array();
public int $insert_id = 99;
public string $last_error = '';
public ?int $last_upserted_balance = null;
public function prepare( string $sql, ...$args ): string {
$i = 0;
return preg_replace_callback( '/%[sd]/', function () use ( &$i, $args ) {
return $args[ $i++ ] ?? '?';
}, $sql );
}
public function get_var( string $sql ): ?string {
$this->queries[] = $sql;
// SELECT … FOR UPDATE simulates an empty membership table (no row).
if ( false !== strpos( $sql, 'FOR UPDATE' ) ) {
return null;
}
// Post-UPSERT re-read returns the balance written by the last INSERT.
if ( null !== $this->last_upserted_balance ) {
return (string) $this->last_upserted_balance;
}
return null;
}
public function get_results( string $sql, $output = 'OBJECT' ): array {
$this->queries[] = $sql;
return array();
}
public function query( string $sql ): int {
$this->queries[] = $sql;
// Capture the delta from INSERT…VALUES(user_id, delta) so subsequent
// re-read SELECTs can return a meaningful balance (mirrors real DB).
if ( preg_match( '/VALUES\s*\(\s*\d+\s*,\s*(-?\d+)\s*\)/', $sql, $m ) ) {
$this->last_upserted_balance = (int) $m[1];
}
return 1;
}
public function insert( string $table, array $data, $format = null ): int {
$this->queries[] = "INSERT {$table}";
return 1;
}
public function update( string $table, array $data, array $where, $format = null, $where_format = null ): int {
$this->queries[] = "UPDATE {$table}";
return 1;
}
public function delete( string $table, array $where, $format = null ): int {
$this->queries[] = "DELETE {$table}";
return 1;
}
public function replace( string $table, array $data, $format = null ): int {
$this->queries[] = "REPLACE {$table}";
return 1;
}
};
}
}