Files
upstream-casbin/ai_api_test.go
upstream-mirror d8971e16fb
Some checks failed
Build / test (1.21) (push) Successful in 2m21s
Build / benchmark (1.21) (push) Failing after 2m28s
golangci-lint / golangci (push) Successful in 19m40s
snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:46:19 +05:00

254 lines
7.1 KiB
Go

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package casbin
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// TestExplainWithoutConfig tests that Explain returns error when config is not set.
func TestExplainWithoutConfig(t *testing.T) {
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
_, err = e.Explain("alice", "data1", "read")
if err == nil {
t.Error("Expected error when AI config is not set")
}
if !strings.Contains(err.Error(), "AI config not set") {
t.Errorf("Expected 'AI config not set' error, got: %v", err)
}
}
// TestExplainWithMockAPI tests Explain with a mock OpenAI-compatible API.
func TestExplainWithMockAPI(t *testing.T) {
// Create a mock server that simulates OpenAI API
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != http.MethodPost {
t.Errorf("Expected POST request, got %s", r.Method)
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
}
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
t.Errorf("Expected Bearer token in Authorization header, got %s", r.Header.Get("Authorization"))
}
// Parse request to verify structure
var req aiChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Errorf("Failed to decode request: %v", err)
}
if req.Model != "gpt-3.5-turbo" {
t.Errorf("Expected model gpt-3.5-turbo, got %s", req.Model)
}
if len(req.Messages) != 2 {
t.Errorf("Expected 2 messages, got %d", len(req.Messages))
}
// Send mock response
resp := aiChatResponse{
Choices: []struct {
Message aiMessage `json:"message"`
}{
{
Message: aiMessage{
Role: "assistant",
Content: "The request was allowed because alice has read permission on data1 according to the policy rule.",
},
},
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config with mock server
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "test-api-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test explanation for allowed request
explanation, err := e.Explain("alice", "data1", "read")
if err != nil {
t.Fatalf("Failed to get explanation: %v", err)
}
if explanation == "" {
t.Error("Expected non-empty explanation")
}
if !strings.Contains(explanation, "allowed") {
t.Errorf("Expected explanation to mention 'allowed', got: %s", explanation)
}
}
// TestExplainDenied tests Explain for a denied request.
func TestExplainDenied(t *testing.T) {
// Create a mock server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := aiChatResponse{
Choices: []struct {
Message aiMessage `json:"message"`
}{
{
Message: aiMessage{
Role: "assistant",
Content: "The request was denied because there is no policy rule that allows alice to write to data1.",
},
},
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "test-api-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test explanation for denied request
explanation, err := e.Explain("alice", "data1", "write")
if err != nil {
t.Fatalf("Failed to get explanation: %v", err)
}
if explanation == "" {
t.Error("Expected non-empty explanation")
}
if !strings.Contains(explanation, "denied") {
t.Errorf("Expected explanation to mention 'denied', got: %s", explanation)
}
}
// TestExplainAPIError tests handling of API errors.
func TestExplainAPIError(t *testing.T) {
// Create a mock server that returns an error
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := aiChatResponse{
Error: &struct {
Message string `json:"message"`
}{
Message: "Invalid API key",
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(resp)
}))
defer mockServer.Close()
// Create enforcer
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Set AI config
e.SetAIConfig(AIConfig{
Endpoint: mockServer.URL,
APIKey: "invalid-key",
Model: "gpt-3.5-turbo",
Timeout: 5 * time.Second,
})
// Test that API error is properly handled
_, err = e.Explain("alice", "data1", "read")
if err == nil {
t.Error("Expected error for API failure")
}
if !strings.Contains(err.Error(), "Invalid API key") {
t.Errorf("Expected API error message, got: %v", err)
}
}
// TestBuildExplainContext tests the context building function.
func TestBuildExplainContext(t *testing.T) {
e, err := NewEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
if err != nil {
t.Fatal(err)
}
// Test with matched rules
rvals := []interface{}{"alice", "data1", "read"}
result := true
matchedRules := []string{"alice, data1, read"}
context := e.buildExplainContext(rvals, result, matchedRules)
// Verify context contains expected elements
if !strings.Contains(context, "alice") {
t.Error("Context should contain subject 'alice'")
}
if !strings.Contains(context, "data1") {
t.Error("Context should contain object 'data1'")
}
if !strings.Contains(context, "read") {
t.Error("Context should contain action 'read'")
}
if !strings.Contains(context, "true") {
t.Error("Context should contain result 'true'")
}
if !strings.Contains(context, "alice, data1, read") {
t.Error("Context should contain matched rule")
}
// Test with no matched rules
context2 := e.buildExplainContext(rvals, false, []string{})
if !strings.Contains(context2, "No policy rules matched") {
t.Error("Context should indicate no matched rules")
}
}