Files
Veyra/internal/integration/provider_test.go
T

152 lines
5.6 KiB
Go

package integration
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"git.sundynix.cn/Blizzard/Veyra/internal/repository"
"git.sundynix.cn/Blizzard/Veyra/internal/secret"
"git.sundynix.cn/Blizzard/Veyra/internal/service"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func testProviderLifecycle(t *testing.T, ctx context.Context, pool *pgxpool.Pool, actor, ordinary model.User) {
t.Helper()
vault, err := secret.NewVault(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{9}, 32)))
if err != nil {
t.Fatal(err)
}
repo := repository.NewProviderRepository(pool)
svc := service.NewProviderService(repo, vault)
configs, err := svc.List(ctx, actor)
if err != nil || len(configs) != 0 {
t.Fatalf("configuration must start empty: %v", err)
}
input := service.ProviderInput{TimeoutSeconds: 15}
config, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
if err != nil || config.Enabled || config.BaseURL != "" || config.LastTestStatus != "NOT_CONFIGURED" || config.WireAPI != "responses" || !config.DisableResponseStorage || !config.RequiresOpenAIAuth {
t.Fatalf("blank configuration: %v", err)
}
if _, err := svc.List(ctx, ordinary); !errors.Is(err, apperror.ErrForbidden) {
t.Fatal("ordinary user accessed provider configuration")
}
if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrConflict) {
t.Fatal("stale configuration accepted")
}
token := "test-private-provider-key"
input = service.ProviderInput{Name: "configured vendor", BaseURL: "https://provider.example/v1", ModelName: "configured-model", ModelCatalogURL: "https://provider.example/v1/models", TimeoutSeconds: 120, Enabled: true, APIKey: &token, ExpectedVersion: config.Version}
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
if err != nil {
t.Fatal(err)
}
if config.TimeoutSeconds != 120 {
t.Fatal("model deadline was not persisted")
}
data, _ := json.Marshal(config)
if bytes.Contains(data, []byte(token)) || bytes.Contains(data, []byte("encrypted_api_key")) {
t.Fatal("API exposes credential")
}
loaded, err := repo.Get(ctx, "model_primary")
if err != nil {
t.Fatal(err)
}
value, err := vault.Decrypt(loaded.EncryptedAPIKey, "model_primary")
if err != nil || value != token {
t.Fatal("credential was not persisted encrypted")
}
input.ExpectedVersion = config.Version
input.ModelCatalogURL = "https://another.example/models"
if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) {
t.Fatal("cross-origin model catalog accepted")
}
input.ModelCatalogURL = "https://provider.example/v1/models"
input.WireAPI = "unknown"
if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) {
t.Fatal("unsupported protocol accepted")
}
input.WireAPI = "responses"
input.ExpectedVersion = config.Version
input.APIKey = nil
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
if err != nil || !config.HasAPIKey {
t.Fatal("omitted key did not preserve existing credential")
}
input.ExpectedVersion = config.Version
input.BaseURL = "https://different.example/v1"
if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) {
t.Fatal("changed endpoint reused existing secret")
}
input.BaseURL = "https://provider.example/v1?api_key=secret"
if _, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input); !errors.Is(err, apperror.ErrInvalidInput) {
t.Fatal("query credentials accepted")
}
input.BaseURL = config.BaseURL
input.Enabled = false
input.ClearAPIKey = true
disabled := false
input.DisableResponseStorage = &disabled
input.RequiresOpenAIAuth = &disabled
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
if err != nil || config.HasAPIKey || config.DisableResponseStorage || config.RequiresOpenAIAuth {
t.Fatal("key clear failed")
}
input.DisableResponseStorage = nil
input.RequiresOpenAIAuth = nil
// Two writers of the same version must produce one successor and one conflict.
input.ExpectedVersion = config.Version
var wg sync.WaitGroup
results := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
results <- err
}()
}
wg.Wait()
close(results)
succeeded, conflicted := 0, 0
for err := range results {
if err == nil {
succeeded++
} else if errors.Is(err, apperror.ErrConflict) {
conflicted++
} else {
t.Fatal(err)
}
}
latest, err := repo.Get(ctx, "model_primary")
if err != nil || latest.DisableResponseStorage || latest.RequiresOpenAIAuth {
t.Fatal("omitted flags did not preserve configuration")
}
if succeeded != 1 || conflicted != 1 {
t.Fatal("concurrent version protection failed")
}
var snapshots string
var historyCount int
if err := pool.QueryRow(ctx, `SELECT count(*),string_agg(snapshot::text,'') FROM veyra_provider_config_history`).Scan(&historyCount, &snapshots); err != nil {
t.Fatal(err)
}
if historyCount != 5 || strings.Contains(snapshots, token) {
t.Fatal("history count or credential sanitization failed")
}
events, err := svc.Audit(ctx, actor)
if err != nil || len(events) != 5 {
t.Fatalf("atomic audit: %v", err)
}
encoded, _ := json.Marshal(events)
if bytes.Contains(encoded, []byte(token)) {
t.Fatal("audit exposed credential")
}
}