diff --git a/docs/server-audit.md b/docs/server-audit.md index 6249899..1fead52 100644 --- a/docs/server-audit.md +++ b/docs/server-audit.md @@ -517,14 +517,20 @@ | `ThinBuilderTest` | insert, select (flat + nested where), update, delete, SQLi identifier/value, транзакции, SQL generation | ✅ 10 тестов | | `RateLimiterTest` | лимиты, блокировка, сброс окна, независимость по IP | ✅ 4 теста | | `PasswordHashTest` | Argon2id verify, SHA1 legacy fallback, rehash detection | ✅ 3 теста | +| `EntityCrudTest` | `Entity::update()`, `get()`, `remove_entity()`, `id()`, `to_array()` | ✅ 6 тестов | +| `AreaPlacingTest` | `Area::place_in_area()`, `place_in_area_id()` | ✅ 2 теста | +| `SessionsTest` | `create()`, `get_session_by_token()`, `close()`, статус | ✅ 4 теста | +| `UtilsTest` | `response_error`, `response_success`, `table_row_is_exists`, `generate_token`, `dayname_translate`, `fast_ping_tcp` | ✅ 7 тестов | -**Итого:** 17 тестов, все проходят. +**Итого:** 36 тестов, все проходят. ### Что ещё нужно покрыть - `DeviceAPI\Base` — retry/backoff (mock cURL). -- `Entity::update()` и транзакции в `Models\Devices`. +- Транзакции в `Models\Devices`. - Валидация в контроллерах (`DevicesRESTAPIController`, `AreasRESTAPIController`). - `App::api_auth_guard()` и `RateLimiter` integration. +- `Area::get_inner_areas()` recursive traversal (глубина ≤10). +- `Area::remove()` — каскадное обнуление `parent_id` и `area_id`. --- @@ -556,4 +562,4 @@ | `ControlScripts/Common.php` | 5.8 | Hardcoded alias'ы | ✅ Вынесено в `sync-map.json` | | `SHServ/.env.example` | 1.3 | — | ✅ Добавлен | | `server/composer.json` | Тесты | — | ✅ PHPUnit 10 | -| `server/tests/` | Тесты | — | ✅ ThinBuilder, RateLimiter, PasswordHash | +| `server/tests/` | Тесты | — | ✅ ThinBuilder, RateLimiter, PasswordHash, Entity, Area, Sessions, Utils | diff --git a/server/tests/AreaPlacingTest.php b/server/tests/AreaPlacingTest.php new file mode 100644 index 0000000..de196b1 --- /dev/null +++ b/server/tests/AreaPlacingTest.php @@ -0,0 +1,82 @@ + thin_builder -> query("CREATE TABLE areas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias VARCHAR(255), + display_name VARCHAR(255), + type VARCHAR(50), + parent_id INTEGER DEFAULT 0, + schema TEXT, + create_at DATETIME, + update_at DATETIME + )"); + } + + private function drop_areas_table(): void { + app() -> thin_builder -> query("DROP TABLE IF EXISTS areas"); + } + + protected function setUp(): void { + $this -> create_areas_table(); + } + + protected function tearDown(): void { + $this -> drop_areas_table(); + } + + public function test_place_in_area_updates_parent_id(): void { + $tb = app() -> thin_builder; + $tb -> insert('areas', [ + 'alias' => 'parent', + 'display_name' => 'Parent', + 'type' => 'room', + 'parent_id' => 0, + 'schema' => '', + 'create_at' => '2024-01-01 10:00:00', + 'update_at' => '2024-01-01 10:00:00', + ]); + $tb -> insert('areas', [ + 'alias' => 'child', + 'display_name' => 'Child', + 'type' => 'room', + 'parent_id' => 0, + 'schema' => '', + 'create_at' => '2024-01-01 10:00:00', + 'update_at' => '2024-01-01 10:00:00', + ]); + + $child = new Area(2); + $parent = new Area(1); + + $result = $child -> place_in_area($parent); + $this -> assertTrue($result); + + $rows = $tb -> select('areas', ['parent_id'], [['id', '=', 2]]); + $this -> assertEquals(1, (int) $rows[0]['parent_id']); + } + + public function test_place_in_area_by_id_updates_parent_id(): void { + $tb = app() -> thin_builder; + $tb -> insert('areas', [ + 'alias' => 'child2', + 'display_name' => 'Child 2', + 'type' => 'room', + 'parent_id' => 0, + 'schema' => '', + 'create_at' => '2024-01-01 10:00:00', + 'update_at' => '2024-01-01 10:00:00', + ]); + + $child = new Area(1); + $result = $child -> place_in_area_id(99); + $this -> assertTrue($result); + + $rows = $tb -> select('areas', ['parent_id'], [['id', '=', 1]]); + $this -> assertEquals(99, (int) $rows[0]['parent_id']); + } +} diff --git a/server/tests/EntityCrudTest.php b/server/tests/EntityCrudTest.php new file mode 100644 index 0000000..3f57432 --- /dev/null +++ b/server/tests/EntityCrudTest.php @@ -0,0 +1,102 @@ + thin_builder -> query("CREATE TABLE areas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias VARCHAR(255), + display_name VARCHAR(255), + type VARCHAR(50), + parent_id INTEGER DEFAULT 0, + schema TEXT, + create_at DATETIME, + update_at DATETIME + )"); + } + + private function drop_areas_table(): void { + app() -> thin_builder -> query("DROP TABLE IF EXISTS areas"); + } + + private function seed_area(array $overrides = []): void { + app() -> thin_builder -> insert('areas', array_merge([ + 'alias' => 'test', + 'display_name' => 'Test', + 'type' => 'room', + 'parent_id' => 0, + 'schema' => '', + 'create_at' => '2024-01-01 10:00:00', + 'update_at' => '2024-01-01 10:00:00', + ], $overrides)); + } + + protected function setUp(): void { + $this -> create_areas_table(); + } + + protected function tearDown(): void { + $this -> drop_areas_table(); + } + + public function test_entity_update_persists_changes(): void { + $this -> seed_area(); + + $area = new Area(1); + $area -> set('display_name', 'Updated Kitchen'); + $result = $area -> update(); + + $this -> assertTrue($result); + + $rows = app() -> thin_builder -> select('areas', ['display_name'], [['id', '=', 1]]); + $this -> assertSame('Updated Kitchen', $rows[0]['display_name']); + } + + public function test_entity_get_returns_field_value(): void { + $this -> seed_area(['alias' => 'bedroom', 'display_name' => 'Bedroom']); + + $area = new Area(1); + $this -> assertSame('bedroom', $area -> get('alias')); + $this -> assertSame('Bedroom', $area -> get('display_name')); + } + + public function test_entity_update_returns_true_when_no_changes(): void { + $this -> seed_area(['alias' => 'hall', 'display_name' => 'Hall']); + + $area = new Area(1); + // No set() called + $this -> assertTrue($area -> update()); + } + + public function test_entity_remove_deletes_row(): void { + $this -> seed_area(['alias' => 'delete_me', 'display_name' => 'Delete Me']); + + $area = new Area(1); + $method = new \ReflectionMethod($area, 'remove_entity'); + $method -> invoke($area); + + $rows = app() -> thin_builder -> select('areas', ['id'], [['id', '=', 1]]); + $this -> assertCount(0, $rows); + } + + public function test_entity_id_returns_correct_value(): void { + $this -> seed_area(); + + $area = new Area(1); + $this -> assertSame(1, $area -> id()); + } + + public function test_entity_to_array_returns_data(): void { + $this -> seed_area(['alias' => 'arr', 'display_name' => 'Array']); + + $area = new Area(1); + $area -> fill(); + + $arr = $area -> to_array(); + + $this -> assertSame('arr', $arr['alias']); + $this -> assertSame('Array', $arr['display_name']); + } +} diff --git a/server/tests/SessionsTest.php b/server/tests/SessionsTest.php new file mode 100644 index 0000000..4d7472c --- /dev/null +++ b/server/tests/SessionsTest.php @@ -0,0 +1,71 @@ + thin_builder -> query("CREATE TABLE sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid INTEGER, + status INTEGER DEFAULT 1, + token VARCHAR(255), + last_using_at DATETIME, + update_at DATETIME, + create_at DATETIME + )"); + } + + private function drop_sessions_table(): void { + app() -> thin_builder -> query("DROP TABLE IF EXISTS sessions"); + } + + protected function setUp(): void { + $this -> create_sessions_table(); + } + + protected function tearDown(): void { + $this -> drop_sessions_table(); + } + + public function test_create_returns_token_and_persists(): void { + $sessions = new \SHServ\Sessions(); + $token = $sessions -> create(42); + + $this -> assertNotFalse($token); + $this -> assertSame(64, strlen($token)); + + $rows = app() -> thin_builder -> select('sessions', ['uid', 'token', 'status'], [['token', '=', $token]]); + $this -> assertCount(1, $rows); + $this -> assertEquals(42, (int) $rows[0]['uid']); + $this -> assertEquals(1, (int) $rows[0]['status']); + } + + public function test_get_session_by_token_returns_entity(): void { + $sessions = new \SHServ\Sessions(); + $token = $sessions -> create(99); + + $session = $sessions -> get_session_by_token($token); + $this -> assertInstanceOf(\SHServ\Entities\Session::class, $session); + $this -> assertEquals(99, (int) $session -> get('uid')); + $this -> assertSame($token, $session -> get('token')); + } + + public function test_get_session_by_invalid_token_returns_null(): void { + $sessions = new \SHServ\Sessions(); + $this -> assertNull($sessions -> get_session_by_token('nonexistent-token')); + } + + public function test_close_updates_status(): void { + $sessions = new \SHServ\Sessions(); + $token = $sessions -> create(7); + + $result = $sessions -> close($token); + $this -> assertTrue($result); + + $session = $sessions -> get_session_by_token($token); + $this -> assertNull($session); + + $rows = app() -> thin_builder -> select('sessions', ['status'], [['token', '=', $token]]); + $this -> assertEquals(2, (int) $rows[0]['status']); + } +} diff --git a/server/tests/TestApp.php b/server/tests/TestApp.php new file mode 100644 index 0000000..a6c6feb --- /dev/null +++ b/server/tests/TestApp.php @@ -0,0 +1,8 @@ + response_error('not_found'); + + $data = json_decode($json, true); + $this -> assertFalse($data['status']); + $this -> assertSame('not_found', $data['error_alias']); + $this -> assertSame('Not found', $data['msg']); + } + + public function test_response_error_includes_failed_fields_and_extra(): void { + $utils = new \SHServ\Utils(); + $json = $utils -> response_error('invalid_request', ['field1'], ['retry_after' => 30], 422); + + $data = json_decode($json, true); + $this -> assertSame(['field1'], $data['failed_fields']); + $this -> assertSame(30, $data['retry_after']); + } + + public function test_response_success_returns_json_with_status_true(): void { + $utils = new \SHServ\Utils(); + $json = $utils -> response_success(['id' => 5]); + + $data = json_decode($json, true); + $this -> assertTrue($data['status']); + $this -> assertSame(['id' => 5], $data['data']); + } + + public function test_table_row_is_exists_finds_row(): void { + $tb = app() -> thin_builder; + $tb -> query("CREATE TABLE IF NOT EXISTS test_exists (id INTEGER PRIMARY KEY, name VARCHAR(50))"); + $tb -> insert('test_exists', ['name' => 'Alice']); + + $utils = new \SHServ\Utils(); + $this -> assertTrue($utils -> table_row_is_exists($tb, 'test_exists', 'name', 'Alice')); + $this -> assertFalse($utils -> table_row_is_exists($tb, 'test_exists', 'name', 'Bob')); + + $tb -> query("DROP TABLE IF EXISTS test_exists"); + } + + public function test_generate_token_length_and_format(): void { + $utils = new \SHServ\Utils(); + + $token = $utils -> generate_token(16); + $this -> assertSame(16, strlen($token)); + $this -> assertMatchesRegularExpression('/^[a-f0-9]+$/', $token); + + $token32 = $utils -> generate_token(32); + $this -> assertSame(32, strlen($token32)); + } + + public function test_dayname_translate(): void { + $utils = new \SHServ\Utils(); + $this -> assertSame('понедельник', $utils -> dayname_translate('monday', 'ru')); + $this -> assertSame('вторник', $utils -> dayname_translate('tuesday', 'ru')); + $this -> assertSame('sunday', $utils -> dayname_translate('sunday', 'en')); + $this -> assertNull($utils -> dayname_translate('unknown', 'ru')); + } + + public function test_fast_ping_tcp_returns_bool(): void { + $utils = new \SHServ\Utils(); + $result = $utils -> fast_ping_tcp('127.0.0.1', 9999, 0.1); + $this -> assertIsBool($result); + } +} diff --git a/server/tests/bootstrap.php b/server/tests/bootstrap.php index 1bafb83..20c99d9 100644 --- a/server/tests/bootstrap.php +++ b/server/tests/bootstrap.php @@ -1,7 +1,39 @@ 'sqlite', + 'dbname' => ':memory:', +], null, false); + +$testApp = new TestApp(); +$testApp->thin_builder = $tb; +$testApp->utils = new \SHServ\Utils(); +$testApp->sessions = new \SHServ\Sessions(); + +\Fury\Kernel\AppContainer::set_app($testApp); + +// Minimal FCONF for message lookups in Utils +if (!defined('FCONF')) { + define('FCONF', [ + 'app_name' => 'TestApp', + 'text_msgs' => [ + 'not_found' => 'Not found', + 'invalid_request' => 'Invalid request', + 'rate_limit_exceeded' => 'Too many requests', + 'unauthorized' => 'Unauthorized', + 'forbidden' => 'Forbidden', + 'device_not_found' => 'Device not found', + 'script_not_exists' => 'Script not found', + 'area_not_found' => 'Area not found', + 'invalid_input' => 'Invalid input', + 'internal_error' => 'Internal server error', + ], + ]); +}