394 lines
11 KiB
Go
394 lines
11 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
)
|
|
|
|
// TestConfig is a sample config struct for testing
|
|
type TestConfig struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type updateStatusOnlyConfigStore struct {
|
|
configstore.ConfigStore
|
|
calls []schemas.KeyStatus
|
|
}
|
|
|
|
type noopTestLogger struct{}
|
|
|
|
func (noopTestLogger) Debug(string, ...any) {}
|
|
func (noopTestLogger) Info(string, ...any) {}
|
|
func (noopTestLogger) Warn(string, ...any) {}
|
|
func (noopTestLogger) Error(string, ...any) {}
|
|
func (noopTestLogger) Fatal(string, ...any) {}
|
|
func (noopTestLogger) SetLevel(schemas.LogLevel) {}
|
|
func (noopTestLogger) SetOutputType(schemas.LoggerOutputType) {}
|
|
func (noopTestLogger) LogHTTPRequest(schemas.LogLevel, string) schemas.LogEventBuilder {
|
|
return schemas.NoopLogEvent
|
|
}
|
|
|
|
func (s *updateStatusOnlyConfigStore) UpdateStatus(ctx context.Context, provider schemas.ModelProvider, keyID string, status, errorMsg string) error {
|
|
s.calls = append(s.calls, schemas.KeyStatus{
|
|
Provider: provider,
|
|
KeyID: keyID,
|
|
Status: schemas.KeyStatusType(status),
|
|
Error: &schemas.BifrostError{Error: &schemas.ErrorField{Message: errorMsg}},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func TestUpdateKeyStatus_KeylessProviderUpdatesProviderStatusInMemory(t *testing.T) {
|
|
prevLogger := logger
|
|
logger = noopTestLogger{}
|
|
defer func() { logger = prevLogger }()
|
|
|
|
store := &updateStatusOnlyConfigStore{}
|
|
server := &BifrostHTTPServer{
|
|
Config: &lib.Config{
|
|
ConfigStore: store,
|
|
Providers: map[schemas.ModelProvider]configstore.ProviderConfig{
|
|
"mock-openai": {
|
|
CustomProviderConfig: &schemas.CustomProviderConfig{IsKeyLess: true},
|
|
Status: "unknown",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
server.updateKeyStatus(context.Background(), []schemas.KeyStatus{{
|
|
Provider: "mock-openai",
|
|
KeyID: "",
|
|
Status: schemas.KeyStatusListModelsFailed,
|
|
Error: &schemas.BifrostError{Error: &schemas.ErrorField{Message: "preview missing model"}},
|
|
}})
|
|
|
|
provider := server.Config.Providers["mock-openai"]
|
|
if provider.Status != string(schemas.KeyStatusListModelsFailed) {
|
|
t.Fatalf("expected provider status %q, got %q", schemas.KeyStatusListModelsFailed, provider.Status)
|
|
}
|
|
if provider.Description != "preview missing model" {
|
|
t.Fatalf("expected provider description to be updated, got %q", provider.Description)
|
|
}
|
|
if len(store.calls) != 1 {
|
|
t.Fatalf("expected one status update call, got %d", len(store.calls))
|
|
}
|
|
if store.calls[0].Provider != "mock-openai" || store.calls[0].KeyID != "" {
|
|
t.Fatalf("expected provider-level status update, got provider=%q keyID=%q", store.calls[0].Provider, store.calls[0].KeyID)
|
|
}
|
|
}
|
|
|
|
func TestUpdateKeyStatus_EmptyKeyIDDoesNotOverwriteKeyedProviderStatus(t *testing.T) {
|
|
prevLogger := logger
|
|
logger = noopTestLogger{}
|
|
defer func() { logger = prevLogger }()
|
|
|
|
store := &updateStatusOnlyConfigStore{}
|
|
server := &BifrostHTTPServer{
|
|
Config: &lib.Config{
|
|
ConfigStore: store,
|
|
Providers: map[schemas.ModelProvider]configstore.ProviderConfig{
|
|
"openai": {
|
|
Keys: []schemas.Key{{ID: "key-1"}},
|
|
Status: "healthy",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
server.updateKeyStatus(context.Background(), []schemas.KeyStatus{{
|
|
Provider: "openai",
|
|
KeyID: "",
|
|
Status: schemas.KeyStatusListModelsFailed,
|
|
Error: &schemas.BifrostError{Error: &schemas.ErrorField{Message: "malformed status"}},
|
|
}})
|
|
|
|
provider := server.Config.Providers["openai"]
|
|
if provider.Status != "healthy" {
|
|
t.Fatalf("expected keyed provider status to remain unchanged, got %q", provider.Status)
|
|
}
|
|
if provider.Description != "" {
|
|
t.Fatalf("expected keyed provider description to remain unchanged, got %q", provider.Description)
|
|
}
|
|
if len(store.calls) != 1 {
|
|
t.Fatalf("expected one status update call, got %d", len(store.calls))
|
|
}
|
|
if store.calls[0].Provider != "openai" || store.calls[0].KeyID != "" {
|
|
t.Fatalf("expected DB status update to retain empty key ID, got provider=%q keyID=%q", store.calls[0].Provider, store.calls[0].KeyID)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithPointerType(t *testing.T) {
|
|
// Test case 1: source is already *T
|
|
expected := &TestConfig{
|
|
Name: "test-plugin",
|
|
Enabled: true,
|
|
Count: 42,
|
|
}
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](expected)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if result != expected {
|
|
t.Errorf("Expected same pointer, got different pointer")
|
|
}
|
|
|
|
if result.Name != expected.Name {
|
|
t.Errorf("Expected Name=%s, got %s", expected.Name, result.Name)
|
|
}
|
|
if result.Enabled != expected.Enabled {
|
|
t.Errorf("Expected Enabled=%v, got %v", expected.Enabled, result.Enabled)
|
|
}
|
|
if result.Count != expected.Count {
|
|
t.Errorf("Expected Count=%d, got %d", expected.Count, result.Count)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithMap(t *testing.T) {
|
|
// Test case 2: source is map[string]any
|
|
configMap := map[string]any{
|
|
"name": "test-plugin",
|
|
"enabled": true,
|
|
"count": 42,
|
|
}
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](configMap)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("Expected non-nil result")
|
|
}
|
|
|
|
if result.Name != "test-plugin" {
|
|
t.Errorf("Expected Name=test-plugin, got %s", result.Name)
|
|
}
|
|
if result.Enabled != true {
|
|
t.Errorf("Expected Enabled=true, got %v", result.Enabled)
|
|
}
|
|
if result.Count != 42 {
|
|
t.Errorf("Expected Count=42, got %d", result.Count)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithString(t *testing.T) {
|
|
// Test case 3: source is string (JSON)
|
|
configStr := `{"name":"test-plugin","enabled":true,"count":42}`
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](configStr)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("Expected non-nil result")
|
|
}
|
|
|
|
if result.Name != "test-plugin" {
|
|
t.Errorf("Expected Name=test-plugin, got %s", result.Name)
|
|
}
|
|
if result.Enabled != true {
|
|
t.Errorf("Expected Enabled=true, got %v", result.Enabled)
|
|
}
|
|
if result.Count != 42 {
|
|
t.Errorf("Expected Count=42, got %d", result.Count)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithInvalidType(t *testing.T) {
|
|
// Test case 4: source is invalid type (should return error)
|
|
invalidSource := 12345
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](invalidSource)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid type, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil result for invalid type, got %v", result)
|
|
}
|
|
|
|
expectedError := "invalid config type"
|
|
if err.Error() != expectedError {
|
|
t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithInvalidJSONString(t *testing.T) {
|
|
// Test case 5: source is string but invalid JSON
|
|
invalidJSON := `{"name":"test-plugin","enabled":true,count:42}` // missing quotes around count
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](invalidJSON)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid JSON, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil result for invalid JSON, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithInvalidMapData(t *testing.T) {
|
|
// Test case 6: source is map but contains invalid data types
|
|
configMap := map[string]any{
|
|
"name": "test-plugin",
|
|
"enabled": "not-a-boolean", // wrong type
|
|
"count": 42,
|
|
}
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](configMap)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid map data, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil result for invalid map data, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithEmptyMap(t *testing.T) {
|
|
// Test case 7: source is empty map (should work, return zero values)
|
|
configMap := map[string]any{}
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](configMap)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error for empty map, got: %v", err)
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("Expected non-nil result")
|
|
}
|
|
|
|
// All fields should have zero values
|
|
if result.Name != "" {
|
|
t.Errorf("Expected empty Name, got %s", result.Name)
|
|
}
|
|
if result.Enabled != false {
|
|
t.Errorf("Expected Enabled=false, got %v", result.Enabled)
|
|
}
|
|
if result.Count != 0 {
|
|
t.Errorf("Expected Count=0, got %d", result.Count)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithEmptyString(t *testing.T) {
|
|
// Test case 8: source is empty string (should fail as invalid JSON)
|
|
configStr := ""
|
|
|
|
result, err := MarshalPluginConfig[TestConfig](configStr)
|
|
if err == nil {
|
|
t.Fatal("Expected error for empty string, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil result for empty string, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithNil(t *testing.T) {
|
|
// Test case 9: source is nil (should return error as invalid type)
|
|
result, err := MarshalPluginConfig[TestConfig](nil)
|
|
if err == nil {
|
|
t.Fatal("Expected error for nil source, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("Expected nil result for nil source, got %v", result)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkMarshalPluginConfig_WithPointerType(b *testing.B) {
|
|
config := &TestConfig{
|
|
Name: "test-plugin",
|
|
Enabled: true,
|
|
Count: 42,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = MarshalPluginConfig[TestConfig](config)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMarshalPluginConfig_WithMap(b *testing.B) {
|
|
configMap := map[string]any{
|
|
"name": "test-plugin",
|
|
"enabled": true,
|
|
"count": 42,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = MarshalPluginConfig[TestConfig](configMap)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMarshalPluginConfig_WithString(b *testing.B) {
|
|
configStr := `{"name":"test-plugin","enabled":true,"count":42}`
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = MarshalPluginConfig[TestConfig](configStr)
|
|
}
|
|
}
|
|
|
|
// Complex config for additional testing
|
|
type ComplexConfig struct {
|
|
Settings map[string]string `json:"settings"`
|
|
Tags []string `json:"tags"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
Nested *TestConfig `json:"nested"`
|
|
}
|
|
|
|
func TestMarshalPluginConfig_WithComplexType(t *testing.T) {
|
|
// Test with a more complex nested structure
|
|
configMap := map[string]any{
|
|
"settings": map[string]any{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
},
|
|
"tags": []any{"tag1", "tag2", "tag3"},
|
|
"metadata": map[string]any{
|
|
"version": "1.0.0",
|
|
"author": "test",
|
|
},
|
|
"nested": map[string]any{
|
|
"name": "nested-config",
|
|
"enabled": true,
|
|
"count": 10,
|
|
},
|
|
}
|
|
|
|
result, err := MarshalPluginConfig[ComplexConfig](configMap)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatal("Expected non-nil result")
|
|
}
|
|
|
|
if len(result.Settings) != 2 {
|
|
t.Errorf("Expected 2 settings, got %d", len(result.Settings))
|
|
}
|
|
if len(result.Tags) != 3 {
|
|
t.Errorf("Expected 3 tags, got %d", len(result.Tags))
|
|
}
|
|
if result.Nested == nil {
|
|
t.Fatal("Expected non-nil nested config")
|
|
}
|
|
if result.Nested.Name != "nested-config" {
|
|
t.Errorf("Expected nested name=nested-config, got %s", result.Nested.Name)
|
|
}
|
|
}
|