Newer
Older
smart-home-server / server / tests / ThinBuilderTest.php
@Eugene Sukhodolskiy Eugene Sukhodolskiy 4 hours ago 5 KB Fix 10 critical/high issues from Phase 6-7 audit
<?php

use PHPUnit\Framework\TestCase;
use Fury\Modules\ThinBuilder\ThinBuilder;

class ThinBuilderTest extends TestCase {
	private ?ThinBuilder $tb = null;

	protected function setUp(): void {
		$this -> tb = new ThinBuilder([
			'dblib' => 'sqlite',
			'dbname' => ':memory:',
		], null, false);

		$this -> create_test_table();
	}

	protected function tearDown(): void {
		$this -> tb = null;
	}

	private function create_test_table(): void {
		$sql = "CREATE TABLE test_table (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			alias VARCHAR(255),
			value VARCHAR(255),
			status VARCHAR(50)
		)";
		$this -> tb -> query($sql);
	}

	public function test_insert_and_select(): void {
		$id = $this -> tb -> insert('test_table', [
			'alias' => 'foo',
			'value' => 'bar',
			'status' => 'active',
		]);

		$this -> assertGreaterThan(0, $id);

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [
			['alias', '=', 'foo']
		]);

		$this -> assertCount(1, $rows);
		$this -> assertSame('foo', $rows[0]['alias']);
		$this -> assertSame('bar', $rows[0]['value']);
	}

	public function test_select_with_flat_where_syntax(): void {
		$this -> tb -> insert('test_table', ['alias' => 'a1', 'value' => 'v1', 'status' => 'ok']);
		$this -> tb -> insert('test_table', ['alias' => 'a2', 'value' => 'v2', 'status' => 'fail']);

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], ['alias', '=', 'a1']);
		$this -> assertCount(1, $rows);
		$this -> assertSame('a1', $rows[0]['alias']);
	}

	public function test_select_multiple_conditions(): void {
		$this -> tb -> insert('test_table', ['alias' => 'x', 'value' => 'y', 'status' => 'active']);
		$this -> tb -> insert('test_table', ['alias' => 'x', 'value' => 'z', 'status' => 'removed']);

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [
			['alias', '=', 'x'],
			'AND',
			['status', '=', 'active']
		]);

		$this -> assertCount(1, $rows);
		$this -> assertSame('y', $rows[0]['value']);
	}

	public function test_update(): void {
		$id = $this -> tb -> insert('test_table', ['alias' => 'old', 'value' => 'val']);

		$updated = $this -> tb -> update('test_table', ['alias' => 'new'], [
			['id', '=', $id]
		]);

		$this -> assertSame(1, $updated);

		$rows = $this -> tb -> select('test_table', ['alias'], [['id', '=', $id]]);
		$this -> assertSame('new', $rows[0]['alias']);
	}

	public function test_delete(): void {
		$id = $this -> tb -> insert('test_table', ['alias' => 'delme', 'value' => 'v']);

		$deleted = $this -> tb -> delete('test_table', [['id', '=', $id]]);
		$this -> assertSame(1, $deleted);

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [['id', '=', $id]]);
		$this -> assertCount(0, $rows);
	}

	public function test_sql_injection_blocked_in_identifier(): void {
		$this -> expectException(\Exception::class);
		$this -> tb -> select('test`; DROP TABLE users; --', ['id']);
	}

	public function test_sql_injection_in_value_is_escaped_by_placeholder(): void {
		$this -> tb -> insert('test_table', [
			'alias' => "foo'; DROP TABLE test_table; --",
			'value' => 'safe',
		]);

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [
			['alias', '=', "foo'; DROP TABLE test_table; --"]
		]);

		$this -> assertCount(1, $rows);
		$this -> assertSame("foo'; DROP TABLE test_table; --", $rows[0]['alias']);

		$stmt = $this -> tb -> query("SELECT COUNT(*) as c FROM test_table");
		$check = $stmt -> fetch(\PDO::FETCH_ASSOC);
		$this -> assertGreaterThanOrEqual(1, $check['c']);
	}

	public function test_transaction_commit(): void {
		$this -> tb -> beginTransaction();
		$this -> tb -> insert('test_table', ['alias' => 'tx', 'value' => 'v']);
		$this -> tb -> commit();

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [['alias', '=', 'tx']]);
		$this -> assertCount(1, $rows);
	}

	public function test_transaction_rollback(): void {
		$this -> tb -> beginTransaction();
		$this -> tb -> insert('test_table', ['alias' => 'tx2', 'value' => 'v']);
		$this -> tb -> rollBack();

		$rows = $this -> tb -> select('test_table', ['id', 'alias', 'value', 'status'], [['alias', '=', 'tx2']]);
		$this -> assertCount(0, $rows);
	}

	public function test_sql_generation_mode(): void {
		$sql = $this -> tb -> sql_for() -> select('test_table', ['id', 'alias'], [['status', '=', 'active']], ['id'], 'ASC', [10]);
		$this -> assertStringContainsString('SELECT', $sql);
		$this -> assertStringContainsString('FROM `test_table`', $sql);
	}

	public function test_where_operator_whitelist_rejects_invalid(): void {
		$this -> expectException(\Exception::class);
		$this -> expectExceptionMessage('Invalid WHERE operator');
		$this -> tb -> select('test_table', ['id'], [['alias', 'OR 1=1 --', 'foo']]);
	}

	public function test_where_in_requires_array(): void {
		$this -> expectException(\Exception::class);
		$this -> expectExceptionMessage('IN operator requires array value');
		$this -> tb -> select('test_table', ['id'], [['alias', 'IN', 'not_an_array']]);
	}

	public function test_short_where_syntax_with_in(): void {
		$this -> tb -> insert('test_table', ['alias' => 'in1', 'value' => 'v1']);
		$this -> tb -> insert('test_table', ['alias' => 'in2', 'value' => 'v2']);
		$this -> tb -> insert('test_table', ['alias' => 'in3', 'value' => 'v3']);

		$rows = $this -> tb -> select('test_table', ['id', 'alias'], ['alias', ['in1', 'in2']]);
		$this -> assertCount(2, $rows);
		$aliases = array_column($rows, 'alias');
		$this -> assertContains('in1', $aliases);
		$this -> assertContains('in2', $aliases);
	}

	public function test_identifier_rejects_numeric_start(): void {
		$this -> expectException(\Exception::class);
		$this -> expectExceptionMessage('Invalid SQL identifier');
		$this -> tb -> select('test_table', ['id'], [['123abc', '=', 'foo']]);
	}
}