New_Micro_Learning/backend/tests/Feature/AI/AiConnectionTest.php

173 خطوط
7.4 KiB
PHP

<?php
namespace Tests\Feature\AI;
use App\Models\User;
use App\Modules\AI\Infrastructure\AiEndpointPolicy;
use App\Modules\Identity\Domain\Enums\UserRole;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AiConnectionTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->app->instance(AiEndpointPolicy::class, new AiEndpointPolicy(
resolver: fn (string $host): array => ['93.184.216.34'],
customProviderHosts: ['ai.example.test'],
));
}
public function test_designer_connects_discovers_and_selects_an_online_model_without_exposing_the_key(): void
{
Http::fake([
'https://ai.example.test/v1/models' => Http::response(['data' => [['id' => 'model-b'], ['id' => 'model-a']]]),
]);
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$connection = $this->postJson('/api/v1/ai-connections', [
'name' => 'سرویس سازمانی',
'provider' => 'openai_compatible',
'mode' => 'online',
'baseUrl' => 'https://ai.example.test/v1',
'apiKey' => 'secret-token',
'defaultModel' => null,
'timeoutSeconds' => 60,
'enabled' => true,
])->assertCreated()->assertJsonPath('data.hasApiKey', true)->assertJsonMissing(['apiKey' => 'secret-token']);
$id = $connection->json('data.id');
$this->postJson("/api/v1/ai-connections/{$id}/models")
->assertOk()
->assertJsonPath('data.models.0', 'model-a');
$this->postJson("/api/v1/ai-connections/{$id}/test")
->assertOk()
->assertJsonPath('data.connected', true);
$row = DB::table('ai_provider_connections')->where('id', $id)->first();
$this->assertNotSame('secret-token', $row->encrypted_api_key);
$this->assertSame('model-a', $row->default_model);
}
public function test_local_connections_allow_http_but_managers_cannot_manage_connections(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$this->postJson('/api/v1/ai-connections', [
'name' => 'Ollama داخلی',
'provider' => 'ollama',
'mode' => 'local',
'baseUrl' => 'http://127.0.0.1:11434/v1',
'defaultModel' => null,
'timeoutSeconds' => 90,
'enabled' => true,
])->assertCreated()->assertJsonPath('data.isDefault', true);
Sanctum::actingAs(User::factory()->create(['organization_id' => $designer->organization_id, 'role' => UserRole::Manager]));
$this->getJson('/api/v1/ai-connections')->assertForbidden();
}
public function test_connection_urls_are_policy_enforced_and_connections_are_tenant_scoped(): void
{
$firstDesigner = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($firstDesigner);
$this->postJson('/api/v1/ai-connections', [
'name' => 'Metadata target',
'provider' => 'ollama',
'mode' => 'local',
'baseUrl' => 'http://169.254.169.254:11434/v1',
'defaultModel' => null,
'timeoutSeconds' => 30,
'enabled' => true,
])->assertUnprocessable()->assertJsonValidationErrors('baseUrl');
$connectionId = $this->postJson('/api/v1/ai-connections', [
'name' => 'Tenant local provider',
'provider' => 'ollama',
'mode' => 'local',
'baseUrl' => 'http://127.0.0.1:11434/v1',
'defaultModel' => null,
'timeoutSeconds' => 30,
'enabled' => true,
])->assertCreated()->json('data.id');
Sanctum::actingAs(User::factory()->create(['role' => UserRole::CourseDesigner]));
$this->postJson("/api/v1/ai-connections/{$connectionId}/test")->assertNotFound();
}
public function test_enabled_and_default_invariant_is_preserved_across_the_connection_lifecycle(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$disabled = $this->postJson('/api/v1/ai-connections', $this->localConnection('Disabled', false))
->assertCreated()
->assertJsonPath('data.enabled', false)
->assertJsonPath('data.isDefault', false)
->json('data.id');
$first = $this->postJson('/api/v1/ai-connections', $this->localConnection('First enabled'))
->assertCreated()
->assertJsonPath('data.isDefault', true)
->json('data.id');
$second = $this->postJson('/api/v1/ai-connections', $this->localConnection('Second enabled'))
->assertCreated()
->assertJsonPath('data.isDefault', false)
->json('data.id');
$this->postJson("/api/v1/ai-connections/{$second}/default")
->assertOk()
->assertJsonPath('data.isDefault', true);
$this->assertDatabaseHas('ai_provider_connections', ['id' => $first, 'is_default' => false]);
$this->patchJson("/api/v1/ai-connections/{$second}", $this->localConnection('Second enabled', false))
->assertOk()
->assertJsonPath('data.enabled', false)
->assertJsonPath('data.isDefault', false);
$this->assertDatabaseHas('ai_provider_connections', ['id' => $first, 'enabled' => true, 'is_default' => true]);
$this->deleteJson("/api/v1/ai-connections/{$first}")->assertNoContent();
$this->assertSame(0, DB::table('ai_provider_connections')->where('organization_id', $designer->organization_id)->where('is_default', true)->count());
$this->assertDatabaseHas('ai_provider_connections', ['id' => $disabled, 'enabled' => false, 'is_default' => false]);
$this->assertDatabaseHas('ai_provider_connections', ['id' => $second, 'enabled' => false, 'is_default' => false]);
}
public function test_deleting_the_default_promotes_another_enabled_connection(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$default = $this->postJson('/api/v1/ai-connections', $this->localConnection('Default'))->assertCreated()->json('data.id');
$replacement = $this->postJson('/api/v1/ai-connections', $this->localConnection('Replacement'))->assertCreated()->json('data.id');
$this->deleteJson("/api/v1/ai-connections/{$default}")->assertNoContent();
$this->assertDatabaseHas('ai_provider_connections', ['id' => $replacement, 'enabled' => true, 'is_default' => true]);
$this->assertSame(1, DB::table('ai_provider_connections')->where('organization_id', $designer->organization_id)->where('enabled', true)->where('is_default', true)->count());
}
/** @return array<string, mixed> */
private function localConnection(string $name, bool $enabled = true): array
{
return [
'name' => $name,
'provider' => 'ollama',
'mode' => 'local',
'baseUrl' => 'http://127.0.0.1:11434/v1',
'defaultModel' => 'qwen',
'timeoutSeconds' => 30,
'enabled' => $enabled,
];
}
}