2
0
Эх сурвалжийг харах

test: 测试改走 pgsql 的 mint_test 库,补 Pest 辅助函数与 AiModel 工厂

迁移文件含 Postgres 专有语句(CREATE EXTENSION "uuid-ossp" 等),
sqlite 内存库跑不起来,故测试也走 pgsql。DB_DATABASE 固定为 mint_test:
RefreshDatabase 会清空目标库,指向开发库会直接毁掉数据。

Pest.php 补 userToken / authHeader / makeStudio / makeChannel /
decodeToken / currentUid,避免每个 feature 测试各造一套夹具。
AiModelFactory 绕开批量赋值保护(AiModel 未声明 $fillable),
而不是为了测试放开生产模型的写入面。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 1 долоо хоног өмнө
parent
commit
abe582b160

+ 60 - 0
api-v13/database/factories/AiModelFactory.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\AiModel;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends Factory<AiModel>
+ */
+class AiModelFactory extends Factory
+{
+    protected $model = AiModel::class;
+
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'uid' => (string) Str::uuid(),
+            'name' => fake()->unique()->slug(2),
+            // real_name 是模型的登录标识,表上有 unique 约束
+            'real_name' => (string) Str::uuid(),
+            'description' => fake()->sentence(),
+            'url' => 'https://api.example.com',
+            'model' => 'gpt-4',
+            'key' => 'sk-'.fake()->uuid(),
+            'system_prompt' => 'you are a helpful assistant',
+            'privacy' => 'private',
+            'owner_id' => (string) Str::uuid(),
+            'editor_id' => (string) Str::uuid(),
+        ];
+    }
+
+    /**
+     * AiModel 没有声明 $fillable(默认 guarded = ['*']),构造器里 fill() 会抛
+     * MassAssignmentException。这里绕开批量赋值保护,而不是为了测试去放开生产模型的写入面。
+     */
+    public function newModel(array $attributes = [])
+    {
+        $model = $this->modelName();
+
+        return (new $model)->forceFill($attributes);
+    }
+
+    /**
+     * 归属于指定 studio(个人 studio 即用户 uid)。
+     */
+    public function ownedBy(string $ownerId): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'owner_id' => $ownerId,
+            'editor_id' => $ownerId,
+        ]);
+    }
+}

+ 7 - 2
api-v13/phpunit.xml

@@ -23,8 +23,13 @@
         <env name="BCRYPT_ROUNDS" value="4"/>
         <env name="BROADCAST_CONNECTION" value="null"/>
         <env name="CACHE_STORE" value="array"/>
-        <env name="DB_CONNECTION" value="sqlite"/>
-        <env name="DB_DATABASE" value=":memory:"/>
+        <!--
+            迁移文件含 Postgres 专有语句(CREATE EXTENSION "uuid-ossp" 等),无法在 sqlite 上跑,
+            故测试也走 pgsql。DB_DATABASE 必须固定为独立的测试库:RefreshDatabase 会清空目标库,
+            指向开发库会直接毁掉数据。
+        -->
+        <env name="DB_CONNECTION" value="pgsql"/>
+        <env name="DB_DATABASE" value="mint_test"/>
         <env name="DB_URL" value=""/>
         <env name="MAIL_MAILER" value="array"/>
         <env name="QUEUE_CONNECTION" value="sync"/>

+ 99 - 0
api-v13/tests/Pest.php

@@ -1,6 +1,13 @@
 <?php
 
+use App\Models\Channel;
+use App\Models\UserInfo;
+use App\Services\AuthService;
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
 use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
 use Tests\TestCase;
 
 /*
@@ -48,3 +55,95 @@ function something()
 {
     // ..
 }
+
+/**
+ * 造一个用户 token。
+ *
+ * AuthService::current() 只解 JWT、不查库,所以测试里不必真的建用户;
+ * payload 结构必须与 AuthService::getUserToken() 保持一致。
+ */
+function userToken(string $userUid, int $userId = 1): string
+{
+    return JWT::encode([
+        'nbf' => time(),
+        'exp' => time() + 3600,
+        'uid' => $userUid,
+        'id' => $userId,
+    ], config('mint.app.jwt_secrets_key'), 'HS512');
+}
+
+/**
+ * 解开一个 token 的 payload。
+ */
+function decodeToken(string $token): object
+{
+    return JWT::decode($token, new Key(config('mint.app.jwt_secrets_key'), 'HS512'));
+}
+
+/**
+ * 把 token 交给 AuthService::current() 判定,返回 user_uid,无效则返回 false。
+ *
+ * 所有端点的鉴权都走这里,故用它来断言「token 是否还有效」。
+ *
+ * @return string|false
+ */
+function currentUid(string $token)
+{
+    $request = Request::create('/', 'GET');
+    $request->headers->set('Authorization', 'Bearer '.$token);
+
+    $user = AuthService::current($request);
+
+    return $user ? $user['user_uid'] : false;
+}
+
+/**
+ * 带用户 token 的请求头。
+ */
+function authHeader(string $userUid): array
+{
+    return ['Authorization' => 'Bearer '.userToken($userUid)];
+}
+
+/**
+ * 建一个用户及其个人 studio,返回 user uid。
+ *
+ * StudioApi::getIdByName() 查的是 user_infos.username,所以 studio 名即用户名。
+ */
+function makeStudio(string $username): string
+{
+    $userId = (string) Str::uuid();
+    (new UserInfo)->forceFill([
+        'userid' => $userId,
+        'username' => $username,
+        'nickname' => $username,
+        'password' => 'x',
+        'email' => $username.'@example.test',
+    ])->save();
+
+    return $userId;
+}
+
+/**
+ * 建一个属于指定用户的 channel,返回 channel uid。
+ *
+ * channels.id 不是自增列,必须显式给值。
+ */
+function makeChannel(string $ownerUid, string $name = 'test channel'): string
+{
+    $uid = (string) Str::uuid();
+    (new Channel)->forceFill([
+        'id' => random_int(1, PHP_INT_MAX),
+        'uid' => $uid,
+        'type' => 'translation',
+        'owner_uid' => $ownerUid,
+        'editor_id' => 0,
+        'name' => $name,
+        'lang' => 'zh-Hans',
+        'status' => 30,
+        'create_time' => time() * 1000,
+        'modify_time' => time() * 1000,
+    ])->save();
+
+    return $uid;
+}