Checks / verify (push) Failing after 9m52s
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
149 lines
5.5 KiB
Go
149 lines
5.5 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: 15, Enabled: true, APIKey: &token, ExpectedVersion: config.Version}
|
|
config, err = svc.Save(ctx, actor, "model_primary", uuid.NewString(), input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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")
|
|
}
|
|
}
|