The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/tsdb/zipkin/client_test.go

389 lines
10 KiB

package zipkin
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/openzipkin/zipkin-go/model"
"github.com/stretchr/testify/assert"
)
func TestZipkinClient_Services(t *testing.T) {
tests := []struct {
name string
mockResponse string
mockStatusCode int
mockStatus string
expectedResult []string
expectError bool
expectedError error
}{
{
name: "Successful response",
mockResponse: `["service1", "service2"]`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{"service1", "service2"},
expectError: false,
expectedError: nil,
},
{
name: "Non-200 response",
mockResponse: "",
mockStatusCode: http.StatusInternalServerError,
mockStatus: "Internal Server Error",
expectedResult: []string{},
expectError: true,
expectedError: backend.DownstreamError(fmt.Errorf("request failed: Internal Server Error")),
},
{
name: "Invalid JSON response",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
mockStatus: "OK",
expectedResult: []string{},
expectError: true,
expectedError: errors.New("invalid character 'i' looking for beginning of value"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v2/services", r.URL.Path)
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
client, _ := New(server.URL, server.Client(), log.New())
services, err := client.Services()
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedResult, services)
})
}
}
func TestZipkinClient_Spans(t *testing.T) {
tests := []struct {
name string
serviceName string
mockResponse string
mockStatusCode int
expectedResult []string
expectError bool
}{
{
name: "Successful response",
serviceName: "service1",
mockResponse: `["span1", "span2"]`,
mockStatusCode: http.StatusOK,
expectedResult: []string{"span1", "span2"},
expectError: false,
},
{
name: "Non-200 response",
serviceName: "service1",
mockResponse: "",
mockStatusCode: http.StatusNotFound,
expectedResult: []string{},
expectError: true,
},
{
name: "Invalid JSON response",
serviceName: "service1",
mockResponse: `{invalid json`,
mockStatusCode: http.StatusOK,
expectedResult: []string{},
expectError: true,
},
{
name: "Empty serviceName",
serviceName: "",
mockResponse: "",
mockStatusCode: http.StatusOK,
expectedResult: []string{},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v2/spans", r.URL.Path)
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
client, _ := New(server.URL, server.Client(), log.New())
spans, err := client.Spans(tt.serviceName)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedResult, spans)
})
}
}
func TestZipkinClient_Traces(t *testing.T) {
tests := []struct {
name string
serviceName string
spanName string
mockResponse interface{}
mockStatusCode int
expectedResult [][]model.SpanModel
expectError bool
expectedError string
}{
{
name: "Successful response",
serviceName: "service1",
spanName: "span1",
mockResponse: [][]model.SpanModel{{{SpanContext: model.SpanContext{TraceID: model.TraceID{Low: 1234}, ID: 1}, Name: "operation1", Tags: map[string]string{"key1": "value1"}}}},
mockStatusCode: http.StatusOK,
expectedResult: [][]model.SpanModel{{{SpanContext: model.SpanContext{TraceID: model.TraceID{Low: 1234}, ID: 1}, Name: "operation1", Tags: map[string]string{"key1": "value1"}}}},
expectError: false,
expectedError: "",
},
{
name: "Non-200 response",
serviceName: "service1",
spanName: "span1",
mockResponse: nil,
mockStatusCode: http.StatusForbidden,
expectedResult: [][]model.SpanModel{},
expectError: true,
expectedError: "EOF",
},
{
name: "Empty serviceName",
serviceName: "",
spanName: "span1",
mockResponse: nil,
mockStatusCode: http.StatusOK,
expectedResult: [][]model.SpanModel{},
expectError: true,
expectedError: "invalid/empty serviceName",
},
{
name: "Empty spanName",
serviceName: "service1",
spanName: "",
mockResponse: nil,
mockStatusCode: http.StatusOK,
expectedResult: [][]model.SpanModel{},
expectError: true,
expectedError: "invalid/empty spanName",
},
{
name: "Valid response with empty trace list",
serviceName: "service1",
spanName: "span1",
mockResponse: [][]model.SpanModel{},
mockStatusCode: http.StatusOK,
expectedResult: [][]model.SpanModel{},
expectError: false,
expectedError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var response []byte
if mockData, ok := tt.mockResponse.([][]model.SpanModel); ok {
response, _ = json.Marshal(mockData)
} else if str, ok := tt.mockResponse.(string); ok {
response = []byte(str)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v2/traces", r.URL.Path)
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write(response)
}))
defer server.Close()
client, _ := New(server.URL, server.Client(), log.New())
traces, err := client.Traces(tt.serviceName, tt.spanName)
if tt.expectError {
assert.Error(t, err)
assert.Equal(t, err.Error(), tt.expectedError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectedResult, traces)
})
}
}
func TestZipkinClient_Trace(t *testing.T) {
tests := []struct {
name string
traceID string
mockResponse string
mockStatusCode int
expectedResult []model.SpanModel
expectError bool
expectedError string
}{
{
name: "Successful response",
traceID: "trace-id",
mockResponse: `[{"traceId":"00000000000004d2","id":"0000000000000001","name":"operation1","tags":{"key1":"value1"}}]`,
mockStatusCode: http.StatusOK,
expectedResult: []model.SpanModel{
{
SpanContext: model.SpanContext{
TraceID: model.TraceID{Low: 1234},
ID: model.ID(1),
},
Name: "operation1",
Tags: map[string]string{"key1": "value1"},
},
},
expectError: false,
expectedError: "",
},
{
name: "Invalid traceID",
traceID: "",
mockResponse: "",
mockStatusCode: http.StatusOK,
expectedResult: []model.SpanModel{},
expectError: true,
expectedError: "invalid/empty traceId",
},
{
name: "Special characters traceID",
traceID: "a/b",
mockResponse: `[{"traceId":"00000000000004d2","id":"0000000000000001","name":"operation1","tags":{"key1":"value1"}}]`,
mockStatusCode: http.StatusOK,
expectedResult: []model.SpanModel{
{
SpanContext: model.SpanContext{
TraceID: model.TraceID{Low: 1234},
ID: model.ID(1),
},
Name: "operation1",
Tags: map[string]string{"key1": "value1"},
},
},
expectError: false,
expectedError: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var client ZipkinClient
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Contains(t, r.URL.String(), "/api/v2/trace/"+url.QueryEscape(tt.traceID))
w.WriteHeader(tt.mockStatusCode)
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
client, _ = New(server.URL, server.Client(), log.New())
trace, err := client.Trace(tt.traceID)
if tt.expectError {
assert.Error(t, err)
assert.Empty(t, trace)
assert.Equal(t, tt.expectedError, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, trace)
}
})
}
}
func TestCreateZipkinURL(t *testing.T) {
tests := []struct {
name string
baseURL string
path string
params map[string]string
expected string
shouldErr bool
}{
{
name: "WithPathAndParams",
baseURL: "http://example.com",
path: "api/v1/trace",
params: map[string]string{"key1": "value1", "key2": "value2"},
expected: "http://example.com/api/v1/trace?key1=value1&key2=value2",
},
{
name: "OnlyParams",
baseURL: "http://example.com",
path: "",
params: map[string]string{"key1": "value1"},
expected: "http://example.com?key1=value1",
},
{
name: "NoParams",
baseURL: "http://example.com",
path: "api/v1/trace",
params: map[string]string{},
expected: "http://example.com/api/v1/trace",
},
{
name: "InvalidBaseURL",
baseURL: "http://example .com",
path: "api/v1/trace",
params: map[string]string{},
shouldErr: true,
},
{
name: "BaseURLWithPath",
baseURL: "http://example.com/base",
path: "api/v1/trace",
params: map[string]string{"key1": "value1"},
expected: "http://example.com/base/api/v1/trace?key1=value1",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := createZipkinURL(tc.baseURL, tc.path, tc.params)
if tc.shouldErr {
if err == nil {
t.Fatalf("Expected error, but got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if result != tc.expected {
t.Errorf("Expected %s, got %s", tc.expected, result)
}
})
}
}