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) } }) } }