Files
grafana/pkg/services/setting/service_test.go
T

504 lines
14 KiB
Go

package setting
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/endpoints/request"
)
func TestRemoteSettingService_ListAsIni(t *testing.T) {
t.Run("should return all settings with empty selector", func(t *testing.T) {
settings := []Setting{
{Section: "server", Key: "port", Value: "3000"},
{Section: "database", Key: "type", Value: "mysql"},
}
server := newTestServer(t, settings, "")
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.ListAsIni(ctx, metav1.LabelSelector{})
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.HasSection("server"))
assert.Equal(t, "3000", result.Section("server").Key("port").String())
assert.True(t, result.HasSection("database"))
assert.Equal(t, "mysql", result.Section("database").Key("type").String())
})
}
func TestRemoteSettingService_List(t *testing.T) {
t.Run("should handle single page response", func(t *testing.T) {
settings := []Setting{
{Section: "server", Key: "port", Value: "3000"},
}
server := newTestServer(t, settings, "")
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, "server", result[0].Section)
assert.Equal(t, "port", result[0].Key)
assert.Equal(t, "3000", result[0].Value)
})
t.Run("should handle multiple settings", func(t *testing.T) {
settings := []Setting{
{Section: "server", Key: "port", Value: "3000"},
{Section: "database", Key: "host", Value: "localhost"},
{Section: "database", Key: "port", Value: "5432"},
}
server := newTestServer(t, settings, "")
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.NoError(t, err)
assert.Len(t, result, 3)
})
t.Run("should handle pagination with continue token", func(t *testing.T) {
// First page
page1Settings := []Setting{
{Section: "section-0", Key: "key-0", Value: "value-0"},
{Section: "section-0", Key: "key-1", Value: "value-1"},
}
// Second page
page2Settings := []Setting{
{Section: "section-1", Key: "key-0", Value: "value-2"},
{Section: "section-1", Key: "key-1", Value: "value-3"},
}
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
continueToken := r.URL.Query().Get("continue")
var settings []Setting
var nextContinue string
if continueToken == "" {
settings = page1Settings
nextContinue = "page2"
} else {
settings = page2Settings
nextContinue = ""
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(generateSettingsJSON(settings, nextContinue)))
}))
defer server.Close()
client := newTestClient(t, server.URL, 2)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.NoError(t, err)
assert.Len(t, result, 4)
assert.Equal(t, 2, requestCount)
})
t.Run("should return error when namespace is missing", func(t *testing.T) {
server := newTestServer(t, nil, "")
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := context.Background() // No namespace
result, err := client.List(ctx, metav1.LabelSelector{})
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "missing namespace")
})
t.Run("should return error on HTTP error", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("internal server error"))
}))
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.Error(t, err)
assert.Nil(t, result)
})
t.Run("should handle API errors", func(t *testing.T) {
statusResponse := `{
"apiVersion": "v1",
"kind": "Status",
"metadata": {},
"status": "Failure",
"message": "settings.setting.grafana.app \"test\" not found",
"reason": "NotFound",
"details": {
"name": "test",
"group": "setting.grafana.app",
"kind": "settings"
},
"code": 404
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(statusResponse))
}))
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "could not find the requested resource")
})
t.Run("should handle 500 internal server error", func(t *testing.T) {
statusResponse := `{
"apiVersion": "v1",
"kind": "Status",
"metadata": {},
"status": "Failure",
"message": "Internal error occurred: database connection failed",
"reason": "InternalError",
"code": 500
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(statusResponse))
}))
defer server.Close()
client := newTestClient(t, server.URL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "error on the server")
})
t.Run("should handle connection errors", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
serverURL := server.URL
server.Close()
client := newTestClient(t, serverURL, 500)
ctx := request.WithNamespace(context.Background(), "test-namespace")
result, err := client.List(ctx, metav1.LabelSelector{})
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "connection refused")
})
}
func TestParseSettingList(t *testing.T) {
t.Run("should parse valid settings list", func(t *testing.T) {
jsonData := `{
"apiVersion": "setting.grafana.app/v0alpha1",
"kind": "SettingList",
"metadata": {"continue": ""},
"items": [
{"spec": {"section": "database", "key": "type", "value": "postgres"}},
{"spec": {"section": "server", "key": "port", "value": "3000"}}
]
}`
settings, continueToken, err := parseSettingList(strings.NewReader(jsonData))
require.NoError(t, err)
assert.Len(t, settings, 2)
assert.Equal(t, "", continueToken)
assert.Equal(t, "database", settings[0].Section)
assert.Equal(t, "type", settings[0].Key)
assert.Equal(t, "postgres", settings[0].Value)
})
t.Run("should parse continue token", func(t *testing.T) {
jsonData := `{
"apiVersion": "setting.grafana.app/v0alpha1",
"kind": "SettingList",
"metadata": {"continue": "next-page-token"},
"items": []
}`
_, continueToken, err := parseSettingList(strings.NewReader(jsonData))
require.NoError(t, err)
assert.Equal(t, "next-page-token", continueToken)
})
t.Run("should handle empty items", func(t *testing.T) {
jsonData := `{
"apiVersion": "setting.grafana.app/v0alpha1",
"kind": "SettingList",
"metadata": {},
"items": []
}`
settings, _, err := parseSettingList(strings.NewReader(jsonData))
require.NoError(t, err)
assert.Len(t, settings, 0)
})
}
func TestToIni(t *testing.T) {
t.Run("should convert settings to ini format", func(t *testing.T) {
settings := []*Setting{
{Section: "database", Key: "type", Value: "postgres"},
{Section: "database", Key: "host", Value: "localhost"},
{Section: "server", Key: "http_port", Value: "3000"},
}
result, err := toIni(settings)
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.HasSection("database"))
assert.True(t, result.HasSection("server"))
assert.Equal(t, "postgres", result.Section("database").Key("type").String())
assert.Equal(t, "localhost", result.Section("database").Key("host").String())
assert.Equal(t, "3000", result.Section("server").Key("http_port").String())
})
t.Run("should handle empty settings list", func(t *testing.T) {
var settings []*Setting
result, err := toIni(settings)
require.NoError(t, err)
assert.NotNil(t, result)
sections := result.Sections()
assert.Len(t, sections, 1) // Only default section
})
t.Run("should handle multiple keys in same section", func(t *testing.T) {
settings := []*Setting{
{Section: "auth", Key: "disable_login_form", Value: "false"},
{Section: "auth", Key: "disable_signout_menu", Value: "true"},
}
result, err := toIni(settings)
require.NoError(t, err)
assert.True(t, result.HasSection("auth"))
authSection := result.Section("auth")
assert.Equal(t, "false", authSection.Key("disable_login_form").String())
assert.Equal(t, "true", authSection.Key("disable_signout_menu").String())
})
}
func TestNew(t *testing.T) {
t.Run("should create client with default page size", func(t *testing.T) {
config := Config{
URL: "https://example.com",
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
}
client, err := New(config)
require.NoError(t, err)
assert.NotNil(t, client)
remoteClient := client.(*remoteSettingService)
assert.Equal(t, DefaultPageSize, remoteClient.pageSize)
})
t.Run("should create client with custom page size", func(t *testing.T) {
config := Config{
URL: "https://example.com",
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
PageSize: 100,
}
client, err := New(config)
require.NoError(t, err)
assert.NotNil(t, client)
remoteClient := client.(*remoteSettingService)
assert.Equal(t, int64(100), remoteClient.pageSize)
})
t.Run("should create client with custom QPS and Burst", func(t *testing.T) {
config := Config{
URL: "https://example.com",
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
QPS: 50.0,
Burst: 100,
}
client, err := New(config)
require.NoError(t, err)
assert.NotNil(t, client)
})
t.Run("should return error when URL is empty", func(t *testing.T) {
config := Config{
URL: "",
}
client, err := New(config)
require.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "URL cannot be empty")
})
t.Run("should return error when auth is not configured", func(t *testing.T) {
config := Config{
URL: "https://example.com",
TokenExchangeClient: nil,
WrapTransport: nil,
}
client, err := New(config)
require.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport")
})
t.Run("should use WrapTransport when provided", func(t *testing.T) {
wrapTransportCalled := false
config := Config{
URL: "https://example.com",
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
wrapTransportCalled = true
return rt
},
}
client, err := New(config)
require.NoError(t, err)
assert.NotNil(t, client)
assert.True(t, wrapTransportCalled)
})
}
// Helper functions
func newTestServer(t *testing.T, settings []Setting, continueToken string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(generateSettingsJSON(settings, continueToken)))
}))
}
func newTestClient(t *testing.T, serverURL string, pageSize int64) Service {
t.Helper()
config := Config{
URL: serverURL,
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
PageSize: pageSize,
}
client, err := New(config)
require.NoError(t, err)
return client
}
func generateSettingsJSON(settings []Setting, continueToken string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf(`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"SettingList","metadata":{"continue":"%s"},"items":[`, continueToken))
for i, s := range settings {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(fmt.Sprintf(
`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"Setting","metadata":{"name":"%s--%s","namespace":"test-namespace"},"spec":{"section":"%s","key":"%s","value":"%s"}}`,
s.Section, s.Key, s.Section, s.Key, s.Value,
))
}
sb.WriteString(`]}`)
return sb.String()
}
// Benchmark tests for streaming JSON parser
func BenchmarkParseSettingList(b *testing.B) {
jsonData := generateSettingListJSON(4000, 100)
jsonBytes := []byte(jsonData)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
reader := bytes.NewReader(jsonBytes)
_, _, _ = parseSettingList(reader)
}
}
func BenchmarkParseSettingList_SinglePage(b *testing.B) {
jsonData := generateSettingListJSON(500, 50)
jsonBytes := []byte(jsonData)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
reader := bytes.NewReader(jsonBytes)
_, _, _ = parseSettingList(reader)
}
}
// generateSettingListJSON generates a K8s-style SettingList JSON response for benchmarks
func generateSettingListJSON(totalSettings, numSections int) string {
var sb strings.Builder
sb.WriteString(`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"SettingList","metadata":{"continue":""},"items":[`)
settingsPerSection := totalSettings / numSections
first := true
for section := 0; section < numSections; section++ {
for key := 0; key < settingsPerSection; key++ {
if !first {
sb.WriteString(",")
}
first = false
sb.WriteString(fmt.Sprintf(
`{"apiVersion":"setting.grafana.app/v0alpha1","kind":"Setting","metadata":{"name":"section-%03d--key-%03d","namespace":"bench-ns"},"spec":{"section":"section-%03d","key":"key-%03d","value":"value-for-section-%d-key-%d"}}`,
section, key, section, key, section, key,
))
}
}
sb.WriteString(`]}`)
return sb.String()
}