瀏覽代碼

Merge pull request #2371 from SSPanel-UIM/dev

Dev 20240225
M1Screw 1 年之前
父節點
當前提交
65dd61d34e

+ 1 - 1
app/predefine.php

@@ -7,4 +7,4 @@ declare(strict_types=1);
  */
 
 const BASE_PATH = __DIR__ . '/..';
-const VERSION = '2023.7';
+const VERSION = '2024.1';

+ 1 - 0
composer.json

@@ -58,6 +58,7 @@
         }
     },
     "require-dev": {
+        "dg/bypass-finals": "^1.6",
         "nunomaduro/phpinsights": "*",
         "phpunit/phpunit": "^10|^11"
     },

+ 113 - 59
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "29d028fe09575d21b97024d48e6f4ebf",
+    "content-hash": "f7e6edab9c07262546075ea15e3c323c",
     "packages": [
         {
             "name": "alipaysdk/openapi",
@@ -124,16 +124,16 @@
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.299.1",
+            "version": "3.300.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457"
+                "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0f87b8e8bfb9afd0ffd702fcda556b465eee457",
-                "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/27d59c22c121ce9c0041c563dc9d7270e180925c",
+                "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c",
                 "shasum": ""
             },
             "require": {
@@ -213,9 +213,9 @@
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.299.1"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.300.4"
             },
-            "time": "2024-02-16T19:08:34+00:00"
+            "time": "2024-02-23T19:10:30+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
@@ -530,16 +530,16 @@
         },
         {
             "name": "composer/ca-bundle",
-            "version": "1.4.0",
+            "version": "1.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/ca-bundle.git",
-                "reference": "b66d11b7479109ab547f9405b97205640b17d385"
+                "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385",
-                "reference": "b66d11b7479109ab547f9405b97205640b17d385",
+                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd",
+                "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd",
                 "shasum": ""
             },
             "require": {
@@ -586,7 +586,7 @@
             "support": {
                 "irc": "irc://irc.freenode.org/composer",
                 "issues": "https://github.com/composer/ca-bundle/issues",
-                "source": "https://github.com/composer/ca-bundle/tree/1.4.0"
+                "source": "https://github.com/composer/ca-bundle/tree/1.4.1"
             },
             "funding": [
                 {
@@ -602,7 +602,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-12-18T12:05:55+00:00"
+            "time": "2024-02-23T10:16:52+00:00"
         },
         {
             "name": "dasprid/enum",
@@ -1201,16 +1201,16 @@
         },
         {
             "name": "illuminate/collections",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/collections.git",
-                "reference": "5cedaba39e331cffd73a01cf27ea83229fa11fba"
+                "reference": "04c24117515ab9b701e4b28506d3f3c15d14bd5f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/collections/zipball/5cedaba39e331cffd73a01cf27ea83229fa11fba",
-                "reference": "5cedaba39e331cffd73a01cf27ea83229fa11fba",
+                "url": "https://api.github.com/repos/illuminate/collections/zipball/04c24117515ab9b701e4b28506d3f3c15d14bd5f",
+                "reference": "04c24117515ab9b701e4b28506d3f3c15d14bd5f",
                 "shasum": ""
             },
             "require": {
@@ -1252,11 +1252,11 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2024-02-09T15:56:19+00:00"
+            "time": "2024-02-13T21:55:58+00:00"
         },
         {
             "name": "illuminate/conditionable",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/conditionable.git",
@@ -1302,7 +1302,7 @@
         },
         {
             "name": "illuminate/container",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/container.git",
@@ -1353,7 +1353,7 @@
         },
         {
             "name": "illuminate/contracts",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/contracts.git",
@@ -1401,16 +1401,16 @@
         },
         {
             "name": "illuminate/database",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/database.git",
-                "reference": "0b851fc41e2ea27f166768694b52ca479fc1e4c7"
+                "reference": "64394348243be403ac9edf21c400077304e005b2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/database/zipball/0b851fc41e2ea27f166768694b52ca479fc1e4c7",
-                "reference": "0b851fc41e2ea27f166768694b52ca479fc1e4c7",
+                "url": "https://api.github.com/repos/illuminate/database/zipball/64394348243be403ac9edf21c400077304e005b2",
+                "reference": "64394348243be403ac9edf21c400077304e005b2",
                 "shasum": ""
             },
             "require": {
@@ -1470,11 +1470,11 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2024-02-13T15:01:42+00:00"
+            "time": "2024-02-20T15:20:15+00:00"
         },
         {
             "name": "illuminate/macroable",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/macroable.git",
@@ -1520,7 +1520,7 @@
         },
         {
             "name": "illuminate/pagination",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/pagination.git",
@@ -1570,16 +1570,16 @@
         },
         {
             "name": "illuminate/support",
-            "version": "v10.44.0",
+            "version": "v10.45.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/illuminate/support.git",
-                "reference": "d757670dcd91b0125d0f3ab82a38cc2ad39d2acf"
+                "reference": "ef80b6c0d0faec648d0625f1bd2b604b61cba5e5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/illuminate/support/zipball/d757670dcd91b0125d0f3ab82a38cc2ad39d2acf",
-                "reference": "d757670dcd91b0125d0f3ab82a38cc2ad39d2acf",
+                "url": "https://api.github.com/repos/illuminate/support/zipball/ef80b6c0d0faec648d0625f1bd2b604b61cba5e5",
+                "reference": "ef80b6c0d0faec648d0625f1bd2b604b61cba5e5",
                 "shasum": ""
             },
             "require": {
@@ -1637,7 +1637,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2024-02-13T14:50:39+00:00"
+            "time": "2024-02-16T10:04:27+00:00"
         },
         {
             "name": "irazasyed/telegram-bot-sdk",
@@ -4422,16 +4422,16 @@
         },
         {
             "name": "stripe/stripe-php",
-            "version": "v13.11.0",
+            "version": "v13.12.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/stripe/stripe-php.git",
-                "reference": "d92a95bd61be5d3141d86986c3b454065f9fcc13"
+                "reference": "8052da9058caae10c7297f85821f652b38e31d85"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/stripe/stripe-php/zipball/d92a95bd61be5d3141d86986c3b454065f9fcc13",
-                "reference": "d92a95bd61be5d3141d86986c3b454065f9fcc13",
+                "url": "https://api.github.com/repos/stripe/stripe-php/zipball/8052da9058caae10c7297f85821f652b38e31d85",
+                "reference": "8052da9058caae10c7297f85821f652b38e31d85",
                 "shasum": ""
             },
             "require": {
@@ -4475,9 +4475,9 @@
             ],
             "support": {
                 "issues": "https://github.com/stripe/stripe-php/issues",
-                "source": "https://github.com/stripe/stripe-php/tree/v13.11.0"
+                "source": "https://github.com/stripe/stripe-php/tree/v13.12.0"
             },
-            "time": "2024-02-16T00:23:50+00:00"
+            "time": "2024-02-22T22:19:58+00:00"
         },
         {
             "name": "symfony/deprecation-contracts",
@@ -6452,18 +6452,71 @@
             },
             "time": "2023-01-05T11:28:13+00:00"
         },
+        {
+            "name": "dg/bypass-finals",
+            "version": "v1.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dg/bypass-finals.git",
+                "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dg/bypass-finals/zipball/efe2fe04bae9f0de271dd462afc049067889e6d1",
+                "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "nette/tester": "^2.3",
+                "phpstan/phpstan": "^0.12"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause",
+                "GPL-2.0",
+                "GPL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "David Grudl",
+                    "homepage": "https://davidgrudl.com"
+                }
+            ],
+            "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes",
+            "keywords": [
+                "finals",
+                "mocking",
+                "phpunit",
+                "testing",
+                "unit"
+            ],
+            "support": {
+                "issues": "https://github.com/dg/bypass-finals/issues",
+                "source": "https://github.com/dg/bypass-finals/tree/v1.6.0"
+            },
+            "time": "2023-11-19T22:19:30+00:00"
+        },
         {
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.49.0",
+            "version": "v3.50.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
-                "reference": "8742f7aa6f72a399688b65e4f58992c2d4681fc2"
+                "reference": "dbea11dcb6d9a1f6c8d51c0e580ab4a8876f524c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/8742f7aa6f72a399688b65e4f58992c2d4681fc2",
-                "reference": "8742f7aa6f72a399688b65e4f58992c2d4681fc2",
+                "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/dbea11dcb6d9a1f6c8d51c0e580ab4a8876f524c",
+                "reference": "dbea11dcb6d9a1f6c8d51c0e580ab4a8876f524c",
                 "shasum": ""
             },
             "require": {
@@ -6473,7 +6526,7 @@
                 "ext-json": "*",
                 "ext-tokenizer": "*",
                 "php": "^7.4 || ^8.0",
-                "sebastian/diff": "^4.0 || ^5.0",
+                "sebastian/diff": "^4.0 || ^5.0 || ^6.0",
                 "symfony/console": "^5.4 || ^6.0 || ^7.0",
                 "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
                 "symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
@@ -6494,7 +6547,8 @@
                 "php-cs-fixer/accessible-object": "^1.1",
                 "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4",
                 "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4",
-                "phpunit/phpunit": "^9.6 || ^10.5.5",
+                "phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2",
+                "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0",
                 "symfony/yaml": "^5.4 || ^6.0 || ^7.0"
             },
             "suggest": {
@@ -6533,7 +6587,7 @@
             ],
             "support": {
                 "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
-                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.49.0"
+                "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.50.0"
             },
             "funding": [
                 {
@@ -6541,7 +6595,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-02-02T00:41:40+00:00"
+            "time": "2024-02-23T23:17:45+00:00"
         },
         {
             "name": "justinrainbow/json-schema",
@@ -6756,16 +6810,16 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v5.0.0",
+            "version": "v5.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc"
+                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2218c2252c874a4624ab2f613d86ac32d227bc69",
+                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69",
                 "shasum": ""
             },
             "require": {
@@ -6808,9 +6862,9 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.1"
             },
-            "time": "2024-01-07T17:17:35+00:00"
+            "time": "2024-02-21T19:24:10+00:00"
         },
         {
             "name": "nunomaduro/phpinsights",
@@ -7088,16 +7142,16 @@
         },
         {
             "name": "phpstan/phpdoc-parser",
-            "version": "1.25.0",
+            "version": "1.26.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpdoc-parser.git",
-                "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240"
+                "reference": "231e3186624c03d7e7c890ec662b81e6b0405227"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240",
-                "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240",
+                "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227",
+                "reference": "231e3186624c03d7e7c890ec662b81e6b0405227",
                 "shasum": ""
             },
             "require": {
@@ -7129,9 +7183,9 @@
             "description": "PHPDoc parser with support for nullable, intersection and generic types",
             "support": {
                 "issues": "https://github.com/phpstan/phpdoc-parser/issues",
-                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0"
+                "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0"
             },
-            "time": "2024-01-04T17:06:16+00:00"
+            "time": "2024-02-23T16:05:55+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",

+ 4 - 0
phpunit.xml

@@ -20,4 +20,8 @@
             <directory>src</directory>
         </include>
     </source>
+
+    <php>
+        <cookie name="testKey" value="testValue"/>
+    </php>
 </phpunit>

+ 0 - 14
src/Command/ClientDownload.php

@@ -32,8 +32,6 @@ final class ClientDownload extends Command
     private array $version;
 
     /**
-     * @return void
-     *
      * @throws GuzzleException
      */
     public function boot(): void
@@ -68,8 +66,6 @@ final class ClientDownload extends Command
      * @param string $fileName
      * @param string $savePath
      * @param string $url
-     *
-     * @return bool
      */
     private function getSourceFile(string $fileName, string $savePath, string $url): bool
     {
@@ -104,8 +100,6 @@ final class ClientDownload extends Command
      *
      * @param string $repo
      *
-     * @return string
-     *
      * @throws GuzzleException
      */
     private function getLatestReleaseTagName(string $repo): string
@@ -125,8 +119,6 @@ final class ClientDownload extends Command
      *
      * @param string $repo
      *
-     * @return string
-     *
      * @throws GuzzleException
      */
     private function getLatestPreReleaseTagName(string $repo): string
@@ -184,8 +176,6 @@ final class ClientDownload extends Command
      * 储存本地软体版本库
      *
      * @param array $versions
-     *
-     * @return void
      */
     private function setLocalVersions(array $versions): void
     {
@@ -204,8 +194,6 @@ final class ClientDownload extends Command
      * @param $name
      * @param $taskName
      * @param $tagName
-     *
-     * @return array|string
      */
     private static function getNames($name, $taskName, $tagName): array|string
     {
@@ -227,8 +215,6 @@ final class ClientDownload extends Command
     /**
      * @param array $task
      *
-     * @return void
-     *
      * @throws GuzzleException
      */
     private function getSoft(array $task): void

+ 0 - 2
src/Controllers/Admin/AnnController.php

@@ -71,8 +71,6 @@ final class AnnController extends BaseController
      * @param ServerRequest $request
      * @param Response $response
      * @param array $args
-     *
-     * @return Response|ResponseInterface
      */
     public function add(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {

+ 0 - 2
src/Controllers/Admin/DocsController.php

@@ -125,8 +125,6 @@ final class DocsController extends BaseController
      * @param ServerRequest $request
      * @param Response $response
      * @param array $args
-     *
-     * @return Response|ResponseInterface
      */
     public function update(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {

+ 0 - 2
src/Controllers/Admin/UserController.php

@@ -108,8 +108,6 @@ final class UserController extends BaseController
      * @param Response $response
      * @param array $args
      *
-     * @return Response|ResponseInterface
-     *
      * @throws Exception
      */
     public function create(ServerRequest $request, Response $response, array $args): Response|ResponseInterface

+ 11 - 0
src/Controllers/Api/AdminApiV1Controller.php

@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\Api;
+
+use App\Controllers\BaseController;
+
+final class AdminApiV1Controller extends BaseController
+{
+}

+ 11 - 0
src/Controllers/Api/NodeApiV1Controller.php

@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\Api;
+
+use App\Controllers\BaseController;
+
+final class NodeApiV1Controller extends BaseController
+{
+}

+ 11 - 0
src/Controllers/Api/UserApiV1Controller.php

@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\Api;
+
+use App\Controllers\BaseController;
+
+final class UserApiV1Controller extends BaseController
+{
+}

+ 0 - 6
src/Controllers/AuthController.php

@@ -57,8 +57,6 @@ final class AuthController extends BaseController
      * @param ServerRequest $request
      * @param Response $response
      * @param array $args
-     *
-     * @return Response|ResponseInterface
      */
     public function loginHandle(ServerRequest $request, Response $response, array $args): Response|ResponseInterface
     {
@@ -209,8 +207,6 @@ final class AuthController extends BaseController
      * @param $money
      * @param $is_admin_reg
      *
-     * @return ResponseInterface
-     *
      * @throws Exception
      */
     public function registerHelper(
@@ -305,8 +301,6 @@ final class AuthController extends BaseController
      * @param Response $response
      * @param array $args
      *
-     * @return Response|ResponseInterface
-     *
      * @throws RedisException
      * @throws Exception
      */

+ 0 - 2
src/Controllers/SubController.php

@@ -25,8 +25,6 @@ final class SubController extends BaseController
      * @param $response
      * @param $args
      *
-     * @return ResponseInterface
-     *
      * @throws ClientExceptionInterface
      * @throws GuzzleException
      * @throws RedisException

+ 3 - 9
src/Controllers/WebAPI/FuncController.php

@@ -15,19 +15,13 @@ final class FuncController extends BaseController
 {
     public function ping(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => 'Pong? Pong!',
-        ]);
+        return ResponseHelper::success($response, 'Pong? Pong!');
     }
 
     public function getDetectRules(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
-        $rules = DetectRule::all();
+        $rules = DetectRule::all()->toArray();
 
-        return ResponseHelper::successWithDataEtag($request, $response, [
-            'ret' => 1,
-            'data' => $rules,
-        ]);
+        return ResponseHelper::successWithDataEtag($request, $response, $rules);
     }
 }

+ 7 - 11
src/Controllers/WebAPI/NodeController.php

@@ -11,6 +11,7 @@ use Psr\Http\Message\ResponseInterface;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
 use function json_decode;
+use const JSON_UNESCAPED_SLASHES;
 use const VERSION;
 
 final class NodeController extends BaseController
@@ -21,17 +22,15 @@ final class NodeController extends BaseController
         $node = (new Node())->find($node_id);
 
         if ($node === null) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node not found.',
-            ]);
+            return ResponseHelper::error($response, 'Node not found.');
+        }
+
+        if ($node->type === 0) {
+            return ResponseHelper::error($response, 'Node is not enabled.');
         }
 
         $data = [
-            'node_group' => $node->node_group,
-            'node_class' => $node->node_class,
             'node_speedlimit' => $node->node_speedlimit,
-            'traffic_rate' => $node->traffic_rate,
             'sort' => $node->sort,
             'server' => $node->server,
             'custom_config' => json_decode($node->custom_config, true, JSON_UNESCAPED_SLASHES),
@@ -39,9 +38,6 @@ final class NodeController extends BaseController
             'version' => VERSION,
         ];
 
-        return ResponseHelper::successWithDataEtag($request, $response, [
-            'ret' => 1,
-            'data' => $data,
-        ]);
+        return ResponseHelper::successWithDataEtag($request, $response, $data);
     }
 }

+ 90 - 127
src/Controllers/WebAPI/UserController.php

@@ -9,7 +9,8 @@ use App\Models\Config;
 use App\Models\DetectLog;
 use App\Models\HourlyUsage;
 use App\Models\Node;
-use App\Services\DB;
+use App\Models\OnlineLog;
+use App\Models\User;
 use App\Services\DynamicRate;
 use App\Utils\ResponseHelper;
 use App\Utils\Tools;
@@ -17,6 +18,7 @@ use Psr\Http\Message\ResponseInterface;
 use Slim\Http\Response;
 use Slim\Http\ServerRequest;
 use function count;
+use function date;
 use function is_array;
 use function json_decode;
 use function time;
@@ -29,8 +31,6 @@ final class UserController extends BaseController
      * @param ServerRequest   $request
      * @param Response  $response
      * @param array     $args
-     *
-     * @return ResponseInterface
      */
     public function index(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
@@ -38,62 +38,50 @@ final class UserController extends BaseController
         $node = (new Node())->find($node_id);
 
         if ($node === null) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node not found.',
-            ]);
+            return ResponseHelper::error($response, 'Node not found.');
         }
 
         if ($node->type === 0) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node is not enabled.',
-            ]);
+            return ResponseHelper::error($response, 'Node is not enabled.');
         }
 
         $node->update(['node_heartbeat' => time()]);
 
         if ($node->node_bandwidth_limit !== 0 && $node->node_bandwidth_limit <= $node->node_bandwidth) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node out of bandwidth.',
-            ]);
+            return ResponseHelper::error($response, 'Node out of bandwidth.');
         }
 
-        $users_raw = DB::select('
-            SELECT
-                user.id,
-                user.u,
-                user.d,
-                user.transfer_enable,
-                user.node_speedlimit,
-                user.node_iplimit,
-                user.method,
-                user.port,
-                user.passwd,
-                user.uuid,
-                IF(online_log.count IS NULL, 0, online_log.count) AS alive_ip
-            FROM
-                user LEFT JOIN (
-                    SELECT
-                        user_id, COUNT(*) AS count
-                    FROM
-                        online_log
-                    WHERE
-                        last_time > UNIX_TIMESTAMP() - 90
-                    GROUP BY
-                        user_id
-                ) AS online_log ON online_log.user_id = user.id
-            WHERE
-                user.is_banned = 0
-                AND user.class_expire > CURRENT_TIMESTAMP()
-                AND (
-                    (
-                        user.class >= ?
-                        AND IF(? = 0, 1, user.node_group = ?)
-                    ) OR user.is_admin = 1
-                )
-        ', [$node->node_class, $node->node_group, $node->node_group]);
+        $users_raw = (new User())->where(
+            'is_banned',
+            0
+        )->where(
+            'class_expire',
+            '>',
+            date('Y-m-d H:i:s')
+        )->where(
+            static function ($query) use ($node): void {
+                $query->where('class', '>=', $node->node_class)
+                    ->where(static function ($query) use ($node): void {
+                        if ($node->node_group !== 0) {
+                            $query->where('node_group', $node->node_group);
+                        }
+                    });
+            }
+        )->orWhere(
+            'is_admin',
+            1
+        )->get([
+            'id',
+            'u',
+            'd',
+            'transfer_enable',
+            'node_speedlimit',
+            'node_iplimit',
+            'method',
+            'port',
+            'passwd',
+            'uuid',
+        ]);
 
         $keys_unset = match ($node->sort) {
             14, 11 => ['u', 'd', 'transfer_enable', 'method', 'port', 'passwd'],
@@ -114,6 +102,16 @@ final class UserController extends BaseController
                 }
             }
 
+            if ($user_raw->node_iplimit !== 0 &&
+                $user_raw->node_iplimit <
+                (new OnlineLog())
+                    ->where('user_id', $user_raw->id)
+                    ->where('last_time', '>', time() - 90)
+                    ->count()
+            ) {
+                continue;
+            }
+
             if ($node->sort === 1) {
                 $method = json_decode($node->custom_config)->method ?? '2022-blake3-aes-128-gcm';
 
@@ -129,13 +127,11 @@ final class UserController extends BaseController
                 unset($user_raw->$key);
             }
 
+            $user_raw->alive_ip = 0;
             $users[] = $user_raw;
         }
 
-        return ResponseHelper::successWithDataEtag($request, $response, [
-            'ret' => 1,
-            'data' => $users,
-        ]);
+        return ResponseHelper::successWithDataEtag($request, $response, $users);
     }
 
     /**
@@ -144,18 +140,13 @@ final class UserController extends BaseController
      * @param ServerRequest   $request
      * @param Response  $response
      * @param array     $args
-     *
-     * @return ResponseInterface
      */
     public function addTraffic(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         $data = json_decode($request->getBody()->__toString());
 
         if (! $data || ! is_array($data->data)) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Invalid data.',
-            ]);
+            return ResponseHelper::error($response, 'Invalid data.');
         }
 
         $data = $data->data;
@@ -163,27 +154,14 @@ final class UserController extends BaseController
         $node = (new Node())->find($node_id);
 
         if ($node === null) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node not found.',
-            ]);
+            return ResponseHelper::error($response, 'Node not found.');
         }
 
         if ($node->type === 0) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node is not enabled.',
-            ]);
+            return ResponseHelper::error($response, 'Node is not enabled.');
         }
 
-        $pdo = DB::getPdo();
-        $stat = $pdo->prepare('
-                UPDATE user SET last_use_time = UNIX_TIMESTAMP(),
-                u = u + ?,
-                d = d + ?,
-                transfer_total = transfer_total + ?,
-                transfer_today = transfer_today + ? WHERE id = ?
-        ');
+        $rate = 1;
 
         if ($node->is_dynamic_rate) {
             $dynamic_rate_config = json_decode($node->dynamic_rate_config);
@@ -214,7 +192,18 @@ final class UserController extends BaseController
             $user_id = $log?->user_id;
 
             if ($user_id) {
-                $stat->execute([(int) ($u * $rate), (int) ($d * $rate), (int) ($u + $d), (int) ($u + $d), (int) $user_id]);
+                $billed_u = $u * $rate;
+                $billed_d = $d * $rate;
+
+                $user = (new User())->find($user_id);
+
+                $user->update([
+                    'last_use_time' => time(),
+                    'u' => $user->u + $billed_u,
+                    'd' => $user->d + $billed_d,
+                    'transfer_total' => $user->transfer_total + $u + $d,
+                    'transfer_today' => $user->transfer_today + $billed_u + $billed_d,
+                ]);
             }
 
             if ($is_traffic_log) {
@@ -224,14 +213,12 @@ final class UserController extends BaseController
             $sum += $u + $d;
         }
 
-        $node->increment('node_bandwidth', $sum);
-        $node->online_user = count($data) - 1;
-        $node->save();
-
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => 'ok',
+        $node->update([
+            'node_bandwidth' => $node->node_bandwidth + $sum,
+            'online_user' => count($data) - 1,
         ]);
+
+        return ResponseHelper::success($response, 'ok');
     }
 
     /**
@@ -240,18 +227,13 @@ final class UserController extends BaseController
      * @param ServerRequest   $request
      * @param Response  $response
      * @param array     $args
-     *
-     * @return ResponseInterface
      */
     public function addAliveIp(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         $data = json_decode($request->getBody()->__toString());
 
         if (! $data || ! is_array($data->data)) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Invalid data.',
-            ]);
+            return ResponseHelper::error($response, 'Invalid data.');
         }
 
         $data = $data->data;
@@ -259,44 +241,39 @@ final class UserController extends BaseController
         $node = (new Node())->find($node_id);
 
         if ($node === null) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node not found.',
-            ]);
+            return ResponseHelper::error($response, 'Node not found.');
         }
 
         if ($node->type === 0) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node is not enabled.',
-            ]);
+            return ResponseHelper::error($response, 'Node is not enabled.');
         }
 
-        $stat = DB::getPdo()->prepare('
-            INSERT INTO online_log (user_id, ip, node_id, first_time, last_time)
-                VALUES (?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())
-                ON DUPLICATE KEY UPDATE node_id = ?, last_time = UNIX_TIMESTAMP()
-        ');
-
         foreach ($data as $log) {
             $ip = (string) $log?->ip;
             $user_id = (int) $log?->user_id;
 
             if (Tools::isIPv4($ip)) {
                 // convert IPv4 Address to IPv4-mapped IPv6 Address
-                $ip = "::ffff:{$ip}";
+                $ip = '::ffff:' . $ip;
             } elseif (! Tools::isIPv6($ip)) {
                 // either IPv4 or IPv6 Address
                 continue;
             }
 
-            $stat->execute([$user_id, $ip, $node_id, $node_id]);
+            (new OnlineLog())->upsert(
+                [
+                    'user_id' => $user_id,
+                    'ip' => $ip,
+                    'node_id' => $node_id,
+                    'first_time' => time(),
+                    'last_time' => time(),
+                ],
+                ['user_id', 'ip'],
+                ['node_id', 'last_time']
+            );
         }
 
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => 'ok',
-        ]);
+        return ResponseHelper::success($response, 'ok');
     }
 
     /**
@@ -305,18 +282,13 @@ final class UserController extends BaseController
      * @param ServerRequest   $request
      * @param Response  $response
      * @param array     $args
-     *
-     * @return ResponseInterface
      */
     public function addDetectLog(ServerRequest $request, Response $response, array $args): ResponseInterface
     {
         $data = json_decode($request->getBody()->__toString());
 
         if (! $data || ! is_array($data->data)) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Invalid data.',
-            ]);
+            return ResponseHelper::error($response, 'Invalid data.');
         }
 
         $data = $data->data;
@@ -324,17 +296,11 @@ final class UserController extends BaseController
         $node = (new Node())->find($node_id);
 
         if ($node === null) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node not found.',
-            ]);
+            return ResponseHelper::error($response, 'Node not found.');
         }
 
         if ($node->type === 0) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => 'Node is not enabled.',
-            ]);
+            return ResponseHelper::error($response, 'Node is not enabled.');
         }
 
         foreach ($data as $log) {
@@ -349,9 +315,6 @@ final class UserController extends BaseController
             ]);
         }
 
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => 'ok',
-        ]);
+        return ResponseHelper::success($response, 'ok');
     }
 }

+ 1 - 3
src/Middleware/AdminApi.php

@@ -16,10 +16,8 @@ final class AdminApi
      * @param ServerRequest $request
      * @param Response $response
      * @param callable $next
-     *
-     * @return ResponseInterface
      */
-    public function __invoke($request, $response, $next)
+    public function __invoke($request, $response, $next): ResponseInterface
     {
     }
 }

+ 1 - 3
src/Middleware/NodeApi.php

@@ -16,10 +16,8 @@ final class NodeApi
      * @param ServerRequest $request
      * @param Response $response
      * @param callable $next
-     *
-     * @return ResponseInterface
      */
-    public function __invoke($request, $response, $next)
+    public function __invoke($request, $response, $next): ResponseInterface
     {
     }
 }

+ 1 - 3
src/Middleware/UserApi.php

@@ -16,10 +16,8 @@ final class UserApi
      * @param ServerRequest $request
      * @param Response $response
      * @param callable $next
-     *
-     * @return ResponseInterface
      */
-    public function __invoke($request, $response, $next)
+    public function __invoke($request, $response, $next): ResponseInterface
     {
     }
 }

+ 0 - 2
src/Models/LoginIp.php

@@ -48,8 +48,6 @@ final class LoginIp extends Model
      * @param string $ip IP
      * @param int $type 1 = failed, 0 = success
      * @param int $user_id User ID
-     *
-     * @return void
      */
     public function collectLoginIP(string $ip, int $type = 0, int $user_id = 0): void
     {

+ 0 - 2
src/Models/User.php

@@ -96,8 +96,6 @@ final class User extends Model
 
     /**
      * @param $len
-     *
-     * @return string
      */
     public function getSs2022Pk($len): string
     {

+ 4 - 6
src/Services/Analytics.php

@@ -24,8 +24,6 @@ final class Analytics
      * 获取累计收入
      *
      * @param string $req
-     *
-     * @return float
      */
     public static function getIncome(string $req): float
     {
@@ -72,7 +70,7 @@ final class Analytics
         return Tools::autoBytes((new User())->sum('transfer_today'));
     }
 
-    public static function getRawTodayTrafficUsage()
+    public static function getRawTodayTrafficUsage(): int
     {
         return (new User())->sum('transfer_today');
     }
@@ -87,7 +85,7 @@ final class Analytics
         return Tools::autoBytes((new User())->sum('u') + (new User())->sum('d') - (new User())->sum('transfer_today'));
     }
 
-    public static function getRawLastTrafficUsage()
+    public static function getRawLastTrafficUsage(): int
     {
         return (new User())->sum('u') + (new User())->sum('d') - (new User())->sum('transfer_today');
     }
@@ -102,7 +100,7 @@ final class Analytics
         return Tools::autoBytes((new User())->sum('transfer_enable') - (new User())->sum('u') - (new User())->sum('d'));
     }
 
-    public static function getRawUnusedTrafficUsage()
+    public static function getRawUnusedTrafficUsage(): int
     {
         return (new User())->sum('transfer_enable') - (new User())->sum('u') - (new User())->sum('d');
     }
@@ -117,7 +115,7 @@ final class Analytics
         return Tools::autoBytes((new User())->sum('transfer_enable'));
     }
 
-    public static function getRawTotalTraffic()
+    public static function getRawTotalTraffic(): int
     {
         return (new User())->sum('transfer_enable');
     }

+ 2 - 0
src/Services/Boot.php

@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace App\Services;
 
+use function date_default_timezone_set;
+use function microtime;
 use function Sentry\init;
 
 final class Boot

+ 0 - 2
src/Services/Subscribe.php

@@ -38,8 +38,6 @@ final class Subscribe
     /**
      * @param $user
      * @param bool $show_all_nodes
-     *
-     * @return Collection
      */
     public static function getUserNodes($user, bool $show_all_nodes = false): Collection
     {

+ 0 - 4
src/Services/View.php

@@ -62,14 +62,10 @@ final class View
         return [
             'appName' => $_ENV['appName'],
             'baseUrl' => $_ENV['baseUrl'],
-
             'jump_delay' => $_ENV['jump_delay'],
-
             'enable_kill' => $_ENV['enable_kill'],
             'enable_change_email' => $_ENV['enable_change_email'],
-
             'enable_r2_client_download' => $_ENV['enable_r2_client_download'],
-
             'jsdelivr_url' => $_ENV['jsdelivr_url'],
         ];
     }

+ 3 - 1
src/Utils/ClassHelper.php

@@ -4,7 +4,10 @@ declare(strict_types=1);
 
 namespace App\Utils;
 
+use function array_filter;
+use function array_keys;
 use function is_null;
+use function strtoupper;
 
 final class ClassHelper
 {
@@ -15,7 +18,6 @@ final class ClassHelper
     {
         self::$composer = null;
         self::$classes = [];
-
         self::$composer = require __DIR__ . '/../../vendor/autoload.php';
 
         if (! is_null(self::$composer)) {

+ 5 - 8
src/Utils/ResponseHelper.php

@@ -24,8 +24,6 @@ final class ResponseHelper
      * @param Response $response
      * @param string $msg
      * @param array $data
-     *
-     * @return ResponseInterface
      */
     public static function successWithData(Response $response, string $msg = '', array $data = []): ResponseInterface
     {
@@ -44,13 +42,11 @@ final class ResponseHelper
      * @param RequestInterface $request
      * @param ResponseInterface $response
      * @param mixed $data
-     *
-     * @return ResponseInterface
      */
     public static function successWithDataEtag(
         RequestInterface $request,
         ResponseInterface $response,
-        mixed $data
+        array $data
     ): ResponseInterface {
         $etag = 'W/"' . hash('xxh64', (string) json_encode($data)) . '"';
 
@@ -58,7 +54,10 @@ final class ResponseHelper
             return $response->withStatus(304);
         }
 
-        return $response->withHeader('ETag', $etag)->withJson($data);
+        return $response->withHeader('ETag', $etag)->withJson([
+            'ret' => 1,
+            'data' => $data,
+        ]);
     }
 
     public static function error(Response $response, string $msg = ''): ResponseInterface
@@ -73,8 +72,6 @@ final class ResponseHelper
      * @param Response $response
      * @param string $msg
      * @param array $data
-     *
-     * @return ResponseInterface
      */
     public static function errorWithData(Response $response, string $msg = '', array $data = []): ResponseInterface
     {

+ 4 - 32
src/Utils/Tools.php

@@ -48,8 +48,6 @@ final class Tools
      * 查询IP归属
      *
      * @param string $ip
-     *
-     * @return string
      */
     public static function getIpLocation(string $ip): string
     {
@@ -77,14 +75,14 @@ final class Tools
             }
         }
 
-        if ($city !== null) {
-            $data = $city . ', ' . $country;
-        }
-
         if ($country !== null) {
             $data = $country;
         }
 
+        if ($city !== null) {
+            $data = $city . ', ' . $country;
+        }
+
         return $data;
     }
 
@@ -93,8 +91,6 @@ final class Tools
      *
      * @param $size
      * @param int $precision
-     *
-     * @return string
      */
     public static function autoBytes($size, int $precision = 2): string
     {
@@ -116,8 +112,6 @@ final class Tools
      * 根据含单位的流量值转换 B 输出
      *
      * @param $size
-     *
-     * @return int|null
      */
     public static function autoBytesR($size): ?int
     {
@@ -141,8 +135,6 @@ final class Tools
      *
      * @param $size
      * @param int $precision
-     *
-     * @return string
      */
     public static function autoMbps($size, int $precision = 2): string
     {
@@ -164,8 +156,6 @@ final class Tools
      * 虽然名字是toMB,但是实际上功能是from MB to B
      *
      * @param $traffic
-     *
-     * @return int
      */
     public static function toMB($traffic): int
     {
@@ -176,8 +166,6 @@ final class Tools
      * 虽然名字是toGB,但是实际上功能是from GB to B
      *
      * @param $traffic
-     *
-     * @return int
      */
     public static function toGB($traffic): int
     {
@@ -186,8 +174,6 @@ final class Tools
 
     /**
      * @param $traffic
-     *
-     * @return float
      */
     public static function flowToMB($traffic): float
     {
@@ -196,8 +182,6 @@ final class Tools
 
     /**
      * @param $traffic
-     *
-     * @return float
      */
     public static function flowToGB($traffic): float
     {
@@ -294,8 +278,6 @@ final class Tools
     /**
      * @param $type
      * @param $str
-     *
-     * @return bool
      */
     public static function isParamValidate($type, $str): bool
     {
@@ -372,8 +354,6 @@ final class Tools
 
     /**
      * @param $input
-     *
-     * @return bool
      */
     public static function isEmail($input): bool
     {
@@ -386,8 +366,6 @@ final class Tools
 
     /**
      * @param $input
-     *
-     * @return bool
      */
     public static function isIPv4($input): bool
     {
@@ -400,8 +378,6 @@ final class Tools
 
     /**
      * @param $input
-     *
-     * @return bool
      */
     public static function isIPv6($input): bool
     {
@@ -414,8 +390,6 @@ final class Tools
 
     /**
      * @param $input
-     *
-     * @return bool
      */
     public static function isInt($input): bool
     {
@@ -431,8 +405,6 @@ final class Tools
      * TODO: Remove this function when PHP 8.3 is minimum requirement and replace it with native function
      *
      * @param string $string
-     *
-     * @return bool
      */
     public static function isJson(string $string): bool
     {

+ 73 - 0
tests/App/Services/ViewTest.php

@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Services;
+
+use PHPUnit\Framework\TestCase;
+use App\Services\View;
+use App\Models\User;
+
+final class ViewTest extends TestCase
+{
+    private View $view;
+    private User $user;
+
+    protected function setUp(): void
+    {
+        $this->view = new View();
+        $this->user = new User();
+    }
+
+    /**
+     * @covers App\Services\View::getTheme
+     */
+    public function testGetTheme(): void
+    {
+        $this->user->isLogin = true;
+        $this->user->theme = 'tabler';
+
+        $theme = $this->view->getTheme($this->user);
+
+        $this->assertEquals('tabler', $theme);
+
+        $_ENV['theme'] = 'not-tabler';
+        $this->user->isLogin = false;
+
+        $theme = $this->view->getTheme($this->user);
+
+        $this->assertEquals('not-tabler', $theme);
+    }
+
+    /**
+     * @covers App\Services\View::getConfig
+     */
+    public function testGetConfig(): void
+    {
+        $_ENV['appName'] = 'Test App';
+        $_ENV['baseUrl'] = 'http://localhost';
+        $_ENV['jump_delay'] = 3;
+        $_ENV['enable_kill'] = true;
+        $_ENV['enable_change_email'] = true;
+        $_ENV['enable_r2_client_download'] = true;
+        $_ENV['jsdelivr_url'] = 'https://cdn.jsdelivr.net';
+
+        $config = $this->view->getConfig();
+
+        $this->assertIsArray($config);
+        $this->assertArrayHasKey('appName', $config);
+        $this->assertArrayHasKey('baseUrl', $config);
+        $this->assertArrayHasKey('jump_delay', $config);
+        $this->assertArrayHasKey('enable_kill', $config);
+        $this->assertArrayHasKey('enable_change_email', $config);
+        $this->assertArrayHasKey('enable_r2_client_download', $config);
+        $this->assertArrayHasKey('jsdelivr_url', $config);
+        $this->assertEquals('Test App', $config['appName']);
+        $this->assertEquals('http://localhost', $config['baseUrl']);
+        $this->assertEquals(3, $config['jump_delay']);
+        $this->assertTrue($config['enable_kill']);
+        $this->assertTrue($config['enable_change_email']);
+        $this->assertTrue($config['enable_r2_client_download']);
+        $this->assertEquals('https://cdn.jsdelivr.net', $config['jsdelivr_url']);
+    }
+}

+ 41 - 0
tests/App/Utils/ClassHelperTest.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Utils;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\ClassHelper;
+
+final class ClassHelperTest extends TestCase
+{
+    private ClassHelper $classHelper;
+
+    protected function setUp(): void
+    {
+        $this->classHelper = new ClassHelper();
+    }
+
+    /**
+     * @covers App\Utils\ClassHelper::getClassesByNamespace
+     */
+    public function testGetClassesByNamespace(): void
+    {
+        $namespace = 'App\\Utils';
+        $classes = $this->classHelper->getClassesByNamespace($namespace);
+
+        $this->assertIsArray($classes);
+        $this->assertContains('\App\Utils\ClassHelper', $classes);
+    }
+
+    /**
+     * @covers App\Utils\ClassHelper::getClasses
+     */
+    public function testGetClasses(): void
+    {
+        $classes = $this->classHelper->getClasses();
+
+        $this->assertIsArray($classes);
+        $this->assertContains('\App\Utils\ClassHelper', $classes);
+    }
+}

+ 51 - 0
tests/App/Utils/CookieTest.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Utils;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Cookie;
+
+final class CookieTest extends TestCase
+{
+    /**
+     * @covers App\Utils\Cookie::set
+     */
+    public function testSet(): void
+    {
+        $data = ['testKey' => 'testValue'];
+        $time = time() + 3600;
+
+        Cookie::set($data, $time);
+
+        $this->assertEquals('testValue', $_COOKIE['testKey']);
+    }
+
+    /**
+     * @covers App\Utils\Cookie::setWithDomain
+     */
+    public function testSetWithDomain(): void
+    {
+        $data = ['testKey' => 'testValue'];
+        $time = time() + 3600;
+        $domain = 'localhost';
+
+        Cookie::setWithDomain($data, $time, $domain);
+
+        $this->assertEquals('testValue', $_COOKIE['testKey']);
+    }
+
+    /**
+     * @covers App\Utils\Cookie::get
+     */
+    public function testGet(): void
+    {
+        $data = ['testKey' => 'testValue'];
+        $time = time() + 3600;
+
+        Cookie::set($data, $time);
+
+        $this->assertEquals('testValue', Cookie::get('testKey'));
+    }
+}

+ 1 - 1
tests/App/Utils/ResponseHelperTest.php

@@ -99,7 +99,7 @@ class ResponseHelperTest extends TestCase
         $result = ResponseHelper::successWithDataEtag($request, $response, $data);
 
         $this->assertEquals(200, $result->getStatusCode());
-        $this->assertEquals('{"foo":"bar"}', (string) $result->getBody());
+        $this->assertEquals('{"ret":1,"data":{"foo":"bar"}}', (string) $result->getBody());
         $this->assertEquals('W/"e929f5f04818d7ec"', $result->getHeaderLine('ETag'));
     }
 }