2
0

AiModelTokenTest.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <?php
  2. use App\Models\AiModel;
  3. use Firebase\JWT\JWT;
  4. use Firebase\JWT\Key;
  5. use Illuminate\Foundation\Testing\RefreshDatabase;
  6. use Illuminate\Support\Str;
  7. uses(RefreshDatabase::class);
  8. it('rejects an anonymous request', function () {
  9. $model = AiModel::factory()->create();
  10. $this->getJson("/api/v2/ai-model-token/{$model->uid}")
  11. ->assertStatus(401);
  12. });
  13. it('rejects a user who does not own the model', function () {
  14. $model = AiModel::factory()->create();
  15. $this->getJson(
  16. "/api/v2/ai-model-token/{$model->uid}",
  17. authHeader((string) Str::uuid())
  18. )->assertStatus(403);
  19. });
  20. it('issues a token to the owner', function () {
  21. $owner = (string) Str::uuid();
  22. $model = AiModel::factory()->ownedBy($owner)->create(['name' => 'claude-opus-5']);
  23. $response = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($owner))
  24. ->assertOk()
  25. ->assertJsonPath('data.uid', $model->uid)
  26. ->assertJsonPath('data.name', 'claude-opus-5');
  27. // 关键断言:签出的 token 代表「模型」而非发起请求的用户。
  28. // 用它写句子时,editor_uid 才会记成模型 uid。
  29. $jwt = JWT::decode(
  30. $response->json('data.token'),
  31. new Key(config('mint.app.jwt_secrets_key'), 'HS512')
  32. );
  33. expect($jwt->uid)->toBe($model->uid);
  34. expect($jwt->exp)->toBeGreaterThan(time());
  35. });
  36. it('issues a model token that expires in 30 days', function () {
  37. $owner = (string) Str::uuid();
  38. $model = AiModel::factory()->ownedBy($owner)->create();
  39. $response = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($owner))
  40. ->assertOk();
  41. $jwt = decodeToken($response->json('data.token'));
  42. // 30 天,不是人类登录 token 的 365 天
  43. expect($jwt->exp - time())->toBeLessThanOrEqual(60 * 60 * 24 * 30)
  44. ->and($jwt->exp - time())->toBeGreaterThan(60 * 60 * 24 * 29);
  45. expect($jwt->typ)->toBe('ai-model');
  46. expect($jwt->ver)->toBe(1);
  47. });
  48. it('404s on an unknown model', function () {
  49. $this->getJson(
  50. '/api/v2/ai-model-token/'.Str::uuid(),
  51. authHeader((string) Str::uuid())
  52. )->assertStatus(404);
  53. });
  54. it('rejects an anonymous revoke', function () {
  55. $model = AiModel::factory()->create();
  56. $this->deleteJson("/api/v2/ai-model-token/{$model->uid}")
  57. ->assertStatus(401);
  58. });
  59. it('rejects a revoke from a user who does not own the model', function () {
  60. $model = AiModel::factory()->create();
  61. $this->deleteJson(
  62. "/api/v2/ai-model-token/{$model->uid}",
  63. [],
  64. authHeader((string) Str::uuid())
  65. )->assertStatus(403);
  66. expect(AiModel::where('uid', $model->uid)->value('token_version'))->toBe(1);
  67. });
  68. it('invalidates issued tokens when the owner revokes them', function () {
  69. $owner = (string) Str::uuid();
  70. $model = AiModel::factory()->ownedBy($owner)->create();
  71. $token = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($owner))
  72. ->assertOk()
  73. ->json('data.token');
  74. expect(currentUid($token))->toBe($model->uid);
  75. $this->deleteJson("/api/v2/ai-model-token/{$model->uid}", [], authHeader($owner))
  76. ->assertOk()
  77. ->assertJsonPath('data.token_version', 2);
  78. // 撤销后旧 token 立刻失效,尽管它的 exp 还在 30 天后
  79. expect(currentUid($token))->toBeFalse();
  80. // 重新签发的 token 带新版本号,可用
  81. $fresh = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($owner))
  82. ->json('data.token');
  83. expect(decodeToken($fresh)->ver)->toBe(2);
  84. expect(currentUid($fresh))->toBe($model->uid);
  85. });
  86. it('rejects a model token whose model has been deleted', function () {
  87. $owner = (string) Str::uuid();
  88. $model = AiModel::factory()->ownedBy($owner)->create();
  89. $token = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($owner))
  90. ->json('data.token');
  91. AiModel::where('uid', $model->uid)->delete();
  92. expect(currentUid($token))->toBeFalse();
  93. });
  94. it('rejects pre-versioning model tokens', function () {
  95. // 引入 token_version 之前签出的模型 token:没有 typ/ver,id 恒为 0,无法撤销
  96. $model = AiModel::factory()->create();
  97. $legacy = JWT::encode([
  98. 'nbf' => time(),
  99. 'exp' => time() + 3600,
  100. 'uid' => $model->uid,
  101. 'id' => 0,
  102. ], config('mint.app.jwt_secrets_key'), 'HS512');
  103. expect(currentUid($legacy))->toBeFalse();
  104. });
  105. it('leaves human tokens alone', function () {
  106. expect(currentUid(userToken('a-user-uid', 42)))->toBe('a-user-uid');
  107. });