Files
grafana/pkg/tsdb/graphite/resource_handler_test.go
Andreas Christou 4e2c3f19df Graphite: Improve functions endpoint (#111902)
Handle HTML response for functions endpoint

- If the endpoint starts with < return an error
- Update tests
- Catch error in FE and use default functions
2025-10-01 20:04:34 +01:00

1271 lines
36 KiB
Go

package graphite
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockRoundTripper struct {
respBody []byte
status int
err error
lastRequest *http.Request
}
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
m.lastRequest = req
if m.err != nil {
return nil, m.err
}
resp := &http.Response{
StatusCode: m.status,
Body: io.NopCloser(bytes.NewBuffer(m.respBody)),
Header: make(http.Header),
}
return resp, nil
}
type mockInstanceManager struct {
instance instancemgmt.Instance
err error
}
func (m *mockInstanceManager) Get(ctx context.Context, pluginCtx backend.PluginContext) (instancemgmt.Instance, error) {
return m.instance, m.err
}
func (m *mockInstanceManager) Dispose(_ string) {}
func (m *mockInstanceManager) Do(ctx context.Context, pluginCtx backend.PluginContext, fn instancemgmt.InstanceCallbackFunc) error {
return nil
}
func TestHandleEvents(t *testing.T) {
mockEvents := []GraphiteEventsResponse{
{When: 1234567890, What: "event1", Tags: []string{"tag1"}, Data: "data1"},
{When: 1234567891, What: "event2", Tags: []string{"tag2"}, Data: "data2"},
}
mockResp, _ := json.Marshal(mockEvents)
tests := []struct {
name string
dsInfo *datasourceInfo
request GraphiteEventsRequest
expectedStatus int
expectError bool
errorContains string
expectedEvents []GraphiteEventsResponse
}{
{
name: "Success with tags",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteEventsRequest{From: "now-1h", Until: "now", Tags: "foo"},
expectedStatus: 200,
expectError: false,
expectedEvents: mockEvents,
},
{
name: "Success without tags",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteEventsRequest{From: "now-1h", Until: "now"},
expectedStatus: 200,
expectError: false,
expectedEvents: mockEvents,
},
{
name: "Invalid URL",
dsInfo: &datasourceInfo{
Id: 1,
URL: "ht tp://invalid url",
},
request: GraphiteEventsRequest{From: "now-1h", Until: "now"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "failed to create events request",
},
{
name: "HTTP client error",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{err: errors.New("network error")}},
},
request: GraphiteEventsRequest{From: "now-1h", Until: "now"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "events request failed",
},
{
name: "Invalid response JSON",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte("invalid json"), status: 200}},
},
request: GraphiteEventsRequest{From: "now-1h", Until: "now"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "events request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &Service{logger: log.NewNullLogger()}
respBody, status, err := svc.handleEvents(context.Background(), tt.dsInfo, &tt.request)
assert.Equal(t, tt.expectedStatus, status)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, respBody)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
assert.NotNil(t, respBody)
if tt.expectedEvents != nil {
var result map[string][]GraphiteEventsResponse
require.NoError(t, json.Unmarshal(respBody, &result))
assert.Equal(t, tt.expectedEvents, result["data"])
}
}
})
}
}
func TestHandleMetricsFind(t *testing.T) {
mockMetrics := []GraphiteMetricsFindResponse{
{Text: "metric1", Id: "metric1.id", AllowChildren: 1, Expandable: 1, Leaf: 0},
{Text: "metric2", Id: "metric2.id", AllowChildren: 0, Expandable: 0, Leaf: 1},
}
mockResp, _ := json.Marshal(mockMetrics)
tests := []struct {
name string
dsInfo *datasourceInfo
request GraphiteMetricsFindRequest
expectedStatus int
expectError bool
errorContains string
expectedMetrics []GraphiteMetricsFindResponse
}{
{
name: "Success with query",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: 200,
expectError: false,
expectedMetrics: mockMetrics,
},
{
name: "Success with query and time range",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteMetricsFindRequest{
Query: "app.grafana.*",
From: "now-1h",
Until: "now",
},
expectedStatus: 200,
expectError: false,
expectedMetrics: mockMetrics,
},
{
name: "Empty query",
dsInfo: &datasourceInfo{Id: 1, URL: "http://graphite.grafana"},
request: GraphiteMetricsFindRequest{Query: ""},
expectedStatus: http.StatusBadRequest,
expectError: true,
errorContains: "query is required",
},
{
name: "Invalid URL",
dsInfo: &datasourceInfo{
Id: 1,
URL: "ht tp://invalid url",
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "failed to create metrics find request",
},
{
name: "HTTP client error",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{err: errors.New("network error")}},
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "metrics find request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &Service{logger: log.NewNullLogger()}
respBody, status, err := svc.handleMetricsFind(context.Background(), tt.dsInfo, &tt.request)
assert.Equal(t, tt.expectedStatus, status)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, respBody)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
assert.NotNil(t, respBody)
if tt.expectedMetrics != nil {
var result []GraphiteMetricsFindResponse
require.NoError(t, json.Unmarshal(respBody, &result))
assert.Equal(t, tt.expectedMetrics, result)
}
}
})
}
}
func TestHandleMetricsExpand(t *testing.T) {
mockExpandResponse := GraphiteMetricsExpandResponse{
Results: []string{"app.grafana.metric1", "app.grafana.metric2", "app.grafana.metric3"},
}
mockResp, _ := json.Marshal(mockExpandResponse)
expectedMetrics := []GraphiteMetricsFindResponse{
{Text: "app.grafana.metric1"},
{Text: "app.grafana.metric2"},
{Text: "app.grafana.metric3"},
}
tests := []struct {
name string
dsInfo *datasourceInfo
request GraphiteMetricsFindRequest
expectedStatus int
expectError bool
errorContains string
expectedMetrics []GraphiteMetricsFindResponse
}{
{
name: "Success with query",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: 200,
expectError: false,
expectedMetrics: expectedMetrics,
},
{
name: "Success with query and time range",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
request: GraphiteMetricsFindRequest{
Query: "app.grafana.*",
From: "now-1h",
Until: "now",
},
expectedStatus: 200,
expectError: false,
expectedMetrics: expectedMetrics,
},
{
name: "Empty query",
dsInfo: &datasourceInfo{Id: 1, URL: "http://graphite.grafana"},
request: GraphiteMetricsFindRequest{Query: ""},
expectedStatus: http.StatusBadRequest,
expectError: true,
errorContains: "query is required",
},
{
name: "Invalid URL",
dsInfo: &datasourceInfo{
Id: 1,
URL: "ht tp://invalid url",
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "failed to create metrics expand request",
},
{
name: "HTTP client error",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{err: errors.New("network error")}},
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "metrics expand request failed",
},
{
name: "Invalid response JSON",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte("invalid json"), status: 200}},
},
request: GraphiteMetricsFindRequest{Query: "app.grafana.*"},
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "metrics expand request failed",
},
{
name: "Empty results",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte(`{"results":[]}`), status: 200}},
},
request: GraphiteMetricsFindRequest{Query: "nonexistent.*"},
expectedStatus: 200,
expectError: false,
expectedMetrics: []GraphiteMetricsFindResponse{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &Service{logger: log.NewNullLogger()}
respBody, status, err := svc.handleMetricsExpand(context.Background(), tt.dsInfo, &tt.request)
assert.Equal(t, tt.expectedStatus, status)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, respBody)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
assert.NotNil(t, respBody)
if tt.expectedMetrics != nil {
var result []GraphiteMetricsFindResponse
require.NoError(t, json.Unmarshal(respBody, &result))
assert.Equal(t, tt.expectedMetrics, result)
}
}
})
}
}
func TestHandleTagsAutocomplete(t *testing.T) {
tests := []struct {
name string
request GraphiteTagsRequest
responseBody string
statusCode int
expectError bool
errorContains string
expectedData []string
}{
{
name: "successful tags autocomplete request",
request: GraphiteTagsRequest{
From: "1h",
Until: "now",
Limit: 10,
TagPrefix: "app",
},
responseBody: `["app", "application", "app_name"]`,
statusCode: 200,
expectedData: []string{"app", "application", "app_name"},
},
{
name: "tags autocomplete with minimal request",
request: GraphiteTagsRequest{},
responseBody: `["tag1", "tag2"]`,
statusCode: 200,
expectedData: []string{"tag1", "tag2"},
},
{
name: "tags autocomplete with empty response",
request: GraphiteTagsRequest{
TagPrefix: "nonexistent",
},
responseBody: `[]`,
statusCode: 200,
expectedData: []string{},
},
{
name: "tags autocomplete server error - invalid JSON causes marshal error",
request: GraphiteTagsRequest{
From: "invalid",
},
responseBody: `invalid json response`,
statusCode: 400,
expectError: true,
errorContains: "tags autocomplete request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockTransport := &mockRoundTripper{
respBody: []byte(tt.responseBody),
status: tt.statusCode,
}
dsInfo := &datasourceInfo{
HTTPClient: &http.Client{Transport: mockTransport},
URL: "http://graphite.example.com",
}
service := &Service{
logger: log.NewNullLogger(),
}
result, statusCode, err := service.handleTagsAutocomplete(context.Background(), dsInfo, &tt.request)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.statusCode, statusCode)
var tags []string
err = json.Unmarshal(result, &tags)
assert.NoError(t, err)
assert.Equal(t, tt.expectedData, tags)
}
if !tt.expectError {
expectedURL := "http://graphite.example.com/tags/autoComplete/tags"
assert.Contains(t, mockTransport.lastRequest.URL.String(), expectedURL)
if tt.request.From != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("from=%s", tt.request.From))
}
if tt.request.Until != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("until=%s", tt.request.Until))
}
if tt.request.Limit != 0 {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("limit=%d", tt.request.Limit))
}
if tt.request.TagPrefix != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("tagPrefix=%s", tt.request.TagPrefix))
}
}
})
}
}
func TestHandleTagValuesAutocomplete(t *testing.T) {
tests := []struct {
name string
request GraphiteTagValuesRequest
responseBody string
statusCode int
expectError bool
errorContains string
expectedData []string
}{
{
name: "successful tag values autocomplete request",
request: GraphiteTagValuesRequest{
Expr: []string{"app=*"},
Tag: "environment",
From: "1h",
Until: "now",
Limit: 5,
ValuePrefix: "prod",
},
responseBody: `["production", "prod-eu", "prod-us"]`,
statusCode: 200,
expectedData: []string{"production", "prod-eu", "prod-us"},
},
{
name: "multiple expressions",
request: GraphiteTagValuesRequest{
Expr: []string{"app=*", "region=us-*"},
Tag: "environment",
From: "1h",
Until: "now",
Limit: 5,
ValuePrefix: "prod",
},
responseBody: `["production", "prod-eu", "prod-us"]`,
statusCode: 200,
expectedData: []string{"production", "prod-eu", "prod-us"},
},
{
name: "tag values autocomplete with empty response",
request: GraphiteTagValuesRequest{
Expr: []string{"app=nonexistent"},
Tag: "environment",
ValuePrefix: "staging",
},
responseBody: `[]`,
statusCode: 200,
expectedData: []string{},
},
{
name: "tag values autocomplete server error",
request: GraphiteTagValuesRequest{
Expr: []string{"invalid-expr"},
Tag: "env",
},
responseBody: `invalid json response`,
statusCode: 400,
expectError: true,
errorContains: "tag values autocomplete request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockTransport := &mockRoundTripper{
respBody: []byte(tt.responseBody),
status: tt.statusCode,
}
dsInfo := &datasourceInfo{
HTTPClient: &http.Client{Transport: mockTransport},
URL: "http://graphite.example.com",
}
service := &Service{
logger: log.NewNullLogger(),
}
result, statusCode, err := service.handleTagValuesAutocomplete(context.Background(), dsInfo, &tt.request)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.statusCode, statusCode)
var tagValues []string
err = json.Unmarshal(result, &tagValues)
assert.NoError(t, err)
assert.Equal(t, tt.expectedData, tagValues)
}
if !tt.expectError {
expectedURL := "http://graphite.example.com/tags/autoComplete/values"
assert.Contains(t, mockTransport.lastRequest.URL.String(), expectedURL)
for _, expr := range tt.request.Expr {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("expr=%s", url.QueryEscape(expr)))
}
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("tag=%s", tt.request.Tag))
if tt.request.From != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("from=%s", tt.request.From))
}
if tt.request.Until != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("until=%s", tt.request.Until))
}
if tt.request.Limit != 0 {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("limit=%d", tt.request.Limit))
}
if tt.request.ValuePrefix != "" {
assert.Contains(t, mockTransport.lastRequest.URL.RawQuery, fmt.Sprintf("valuePrefix=%s", tt.request.ValuePrefix))
}
}
})
}
}
func TestHandleVersion(t *testing.T) {
tests := []struct {
name string
responseBody string
statusCode int
expectError bool
errorContains string
expectedData string
}{
{
name: "successful version request",
responseBody: `"1.1.10"`,
statusCode: 200,
expectedData: "1.1.10",
},
{
name: "version with build info",
responseBody: `"1.1.10-pre1"`,
statusCode: 200,
expectedData: "1.1.10-pre1",
},
{
name: "version request server error - invalid JSON causes parse error",
responseBody: `{"error": "internal error"}`,
statusCode: 500,
expectError: true,
errorContains: "version request failed",
},
{
name: "version request not found - invalid JSON causes parse error",
responseBody: `{"error": "not found"}`,
statusCode: 404,
expectError: true,
errorContains: "version request failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockTransport := &mockRoundTripper{
respBody: []byte(tt.responseBody),
status: tt.statusCode,
}
dsInfo := &datasourceInfo{
HTTPClient: &http.Client{Transport: mockTransport},
URL: "http://graphite.example.com",
}
service := &Service{
logger: log.NewNullLogger(),
}
result, statusCode, err := service.handleVersion(context.Background(), dsInfo, nil)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.statusCode, statusCode)
var version string
err = json.Unmarshal(result, &version)
assert.NoError(t, err)
assert.Equal(t, tt.expectedData, version)
}
if !tt.expectError {
expectedURL := "http://graphite.example.com/version"
assert.Equal(t, expectedURL, mockTransport.lastRequest.URL.String())
assert.Equal(t, http.MethodGet, mockTransport.lastRequest.Method)
}
})
}
}
func TestHandleFunctions(t *testing.T) {
tests := []struct {
name string
responseBody string
statusCode int
expectError bool
errorContains string
expectedData string
}{
{
name: "successful functions request",
responseBody: `{"sum": {"description": "Sum function"}, "avg": {"description": "Average function"}}`,
statusCode: 200,
expectError: false,
expectedData: `{"sum": {"description": "Sum function"}, "avg": {"description": "Average function"}}`,
},
{
name: "functions with infinity replacement",
responseBody: `{"func": {"default": Infinity, "description": "Test function"}}`,
statusCode: 200,
expectError: false,
expectedData: `{"func": {"default": 1e9999, "description": "Test function"}}`,
},
{
name: "empty functions response",
responseBody: `{}`,
statusCode: 200,
expectError: false,
expectedData: `{}`,
},
{
name: "functions request server error",
responseBody: `{"error": "internal error"}`,
statusCode: 500,
expectError: true,
errorContains: "functions request failed",
},
{
name: "functions request not found",
responseBody: `{"error": "not found"}`,
statusCode: 404,
expectError: true,
errorContains: "functions request failed",
},
{
name: "network error",
responseBody: "",
statusCode: 0,
expectError: true,
errorContains: "functions request failed",
},
{
name: "html response",
responseBody: `<html>
<head>
<title>Graphite Browser</title>
</head>
<frameset rows="60,*" frameborder="1" border="1">
<frame src="/browser/header/" name="Header" id='header' scrolling="no" noresize="true" />
<frame src="/composer/?" name="content" id="composerFrame"/>
</frameset>
</html>
`,
statusCode: 200,
expectError: true,
errorContains: "invalid functions response received from Graphite",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var mockTransport *mockRoundTripper
if tt.name == "network error" {
mockTransport = &mockRoundTripper{
err: errors.New("network connection failed"),
}
} else {
mockTransport = &mockRoundTripper{
respBody: []byte(tt.responseBody),
status: tt.statusCode,
}
}
dsInfo := &datasourceInfo{
HTTPClient: &http.Client{Transport: mockTransport},
URL: "http://graphite.example.com",
}
service := &Service{
logger: log.NewNullLogger(),
}
result, statusCode, err := service.handleFunctions(context.Background(), dsInfo, nil)
if tt.expectError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.statusCode, statusCode)
assert.Equal(t, tt.expectedData, string(result))
}
// Verify the request was made correctly (except for network error case)
if tt.name != "network error" {
require.NotNil(t, mockTransport.lastRequest)
assert.Equal(t, "http://graphite.example.com/functions", mockTransport.lastRequest.URL.String())
assert.Equal(t, http.MethodGet, mockTransport.lastRequest.Method)
}
})
}
}
func TestHandleResourceReq_Success(t *testing.T) {
mockEvents := []GraphiteEventsResponse{{When: 1234567890, What: "event1"}}
mockResp, _ := json.Marshal(mockEvents)
dsInfo := datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
}
svc := &Service{
logger: log.NewNullLogger(),
im: &mockInstanceManager{instance: dsInfo},
}
request := GraphiteEventsRequest{From: "now-1h", Until: "now"}
requestBody, _ := json.Marshal(request)
req := httptest.NewRequest("POST", "/events", bytes.NewBuffer(requestBody))
req = req.WithContext(backend.WithPluginContext(context.Background(), backend.PluginContext{}))
rr := httptest.NewRecorder()
handler := handleResourceReq(svc.handleEvents, svc)
handler(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var result map[string][]GraphiteEventsResponse
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &result))
assert.Equal(t, mockEvents, result["data"])
}
func TestHandleResourceReq_GetDSInfoError(t *testing.T) {
svc := &Service{
logger: log.NewNullLogger(),
im: &mockInstanceManager{err: errors.New("datasource not found")},
}
req := httptest.NewRequest("POST", "/events", bytes.NewBufferString("{}"))
req = req.WithContext(backend.WithPluginContext(context.Background(), backend.PluginContext{}))
rr := httptest.NewRecorder()
handler := handleResourceReq(svc.handleEvents, svc)
handler(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
var errorResp map[string]string
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &errorResp))
assert.Contains(t, errorResp["error"], "unexpected error")
}
func TestHandleResourceReq_NilHandler(t *testing.T) {
dsInfo := datasourceInfo{Id: 1, URL: "http://graphite.grafana"}
svc := &Service{
logger: log.NewNullLogger(),
im: &mockInstanceManager{instance: dsInfo},
}
req := httptest.NewRequest("POST", "/events", bytes.NewBufferString("{}"))
req = req.WithContext(backend.WithPluginContext(context.Background(), backend.PluginContext{}))
rr := httptest.NewRecorder()
handler := handleResourceReq[any](nil, svc)
handler(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
var errorResp map[string]string
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &errorResp))
assert.Equal(t, "responseFn should not be nil", errorResp["error"])
}
func TestWriteErrorResponse(t *testing.T) {
rr := httptest.NewRecorder()
writeErrorResponse(rr, http.StatusBadRequest, "test error message")
assert.Equal(t, http.StatusBadRequest, rr.Code)
var errorResp map[string]string
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &errorResp))
assert.Equal(t, "test error message", errorResp["error"])
}
func TestDoGraphiteRequest(t *testing.T) {
mockResponse := []GraphiteEventsResponse{
{When: 1234567890, What: "event1", Tags: []string{"tag1"}, Data: "data1"},
}
mockResp, _ := json.Marshal(mockResponse)
tests := []struct {
name string
endpoint string
dsInfo *datasourceInfo
method string
body io.Reader
headers map[string]string
expectedStatus int
expectError bool
errorContains string
expectedData []GraphiteEventsResponse
}{
{
name: "Success GET request",
endpoint: "events",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
method: "GET",
headers: map[string]string{"Content-Type": "application/json"},
expectedStatus: 200,
expectError: false,
expectedData: mockResponse,
},
{
name: "Success POST request with body",
endpoint: "events",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
method: "POST",
body: bytes.NewReader([]byte("query=test")),
headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
expectedStatus: 200,
expectError: false,
expectedData: mockResponse,
},
{
name: "HTTP client error",
endpoint: "events",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{err: errors.New("network error")}},
},
method: "GET",
headers: map[string]string{},
expectError: true,
errorContains: "failed to complete request",
},
{
name: "Invalid response JSON",
endpoint: "events",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte("invalid json"), status: 200}},
},
method: "GET",
headers: map[string]string{},
expectError: true,
errorContains: "failed to parse response",
},
{
name: "Non-200 status code with valid JSON",
endpoint: "events",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte("[]"), status: 500}},
},
method: "GET",
headers: map[string]string{},
expectError: true,
errorContains: "request failed, status: 500",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
// Create a service instance for the test
svc := &Service{logger: log.NewNullLogger()}
// Create the HTTP request using the createRequest method
req, err := svc.createRequest(ctx, tt.dsInfo, URLParams{
SubPath: tt.endpoint,
Method: tt.method,
Body: tt.body,
Headers: tt.headers,
})
if tt.expectError {
// For cases where we expect errors in request creation
if err != nil {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
return
}
} else {
assert.NoError(t, err)
}
result, _, status, err := doGraphiteRequest[[]GraphiteEventsResponse](ctx, tt.dsInfo, svc.logger, req, false)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, result)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
if tt.expectedStatus != 0 {
assert.Equal(t, tt.expectedStatus, status)
}
if tt.expectedData != nil {
assert.Equal(t, tt.expectedData, *result)
}
}
})
}
}
func TestDoGraphiteRequestGenericTypes(t *testing.T) {
// Test with GraphiteMetricsFindResponse
mockMetrics := []GraphiteMetricsFindResponse{
{Text: "metric1", Id: "metric1.id", AllowChildren: 1, Expandable: 1, Leaf: 0},
}
mockMetricsResp, _ := json.Marshal(mockMetrics)
// Test with GraphiteMetricsExpandResponse
mockExpand := GraphiteMetricsExpandResponse{
Results: []string{"app.grafana.metric1", "app.grafana.metric2"},
}
mockExpandResp, _ := json.Marshal(mockExpand)
tests := []struct {
name string
testFunc func(t *testing.T)
}{
{
name: "Success with GraphiteMetricsFindResponse type",
testFunc: func(t *testing.T) {
dsInfo := &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockMetricsResp, status: 200}},
}
ctx := context.Background()
// Create a service instance for the test
svc := &Service{logger: log.NewNullLogger()}
// Create the HTTP request using the createRequest method
req, err := svc.createRequest(ctx, dsInfo, URLParams{
SubPath: "test",
Method: "GET",
})
assert.NoError(t, err)
result, _, status, err := doGraphiteRequest[[]GraphiteMetricsFindResponse](ctx, dsInfo, svc.logger, req, false)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, 200, status)
assert.Equal(t, mockMetrics, *result)
},
},
{
name: "Success with GraphiteMetricsExpandResponse type",
testFunc: func(t *testing.T) {
dsInfo := &datasourceInfo{
Id: 1,
URL: "http://graphite.grafana",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockExpandResp, status: 200}},
}
ctx := context.Background()
// Create a service instance for the test
svc := &Service{logger: log.NewNullLogger()}
// Create the HTTP request using the createRequest method
req, err := svc.createRequest(ctx, dsInfo, URLParams{
SubPath: "test",
Method: "GET",
})
assert.NoError(t, err)
result, _, status, err := doGraphiteRequest[GraphiteMetricsExpandResponse](ctx, dsInfo, svc.logger, req, false)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, 200, status)
assert.Equal(t, mockExpand, *result)
},
},
}
for _, tt := range tests {
t.Run(tt.name, tt.testFunc)
}
}
func TestParseRequestBody(t *testing.T) {
tests := []struct {
name string
requestBody []byte
expectError bool
errorContains string
expectedData GraphiteEventsRequest
}{
{
name: "Valid JSON request",
requestBody: []byte(`{"from": "now-1h", "until": "now", "tags": "app.grafana"}`),
expectError: false,
expectedData: GraphiteEventsRequest{From: "now-1h", Until: "now", Tags: "app.grafana"},
},
{
name: "Empty JSON object",
requestBody: []byte(`{}`),
expectError: false,
expectedData: GraphiteEventsRequest{},
},
{
name: "Invalid JSON",
requestBody: []byte(`{"invalid": json}`),
expectError: true,
errorContains: "unexpected error",
},
{
name: "Empty request body",
requestBody: []byte(``),
expectError: true,
errorContains: "unexpected error",
},
{
name: "Malformed JSON",
requestBody: []byte(`{"from": "now-1h", "until": }`),
expectError: true,
errorContains: "unexpected error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := log.NewNullLogger()
result, err := parseRequestBody[GraphiteEventsRequest](tt.requestBody, logger)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, result)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedData, *result)
}
})
}
}
func TestParseResponse(t *testing.T) {
mockEvents := []GraphiteEventsResponse{
{When: 1234567890, What: "event1", Tags: []string{"tag1"}, Data: "data1"},
{When: 1234567891, What: "event2", Tags: []string{"tag2"}, Data: "data2"},
}
mockResp, _ := json.Marshal(mockEvents)
tests := []struct {
name string
response *http.Response
expectError bool
errorContains string
expectedData []GraphiteEventsResponse
}{
{
name: "Valid JSON response",
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer(mockResp)),
Header: make(http.Header),
},
expectError: false,
expectedData: mockEvents,
},
{
name: "Empty JSON array",
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer([]byte("[]"))),
Header: make(http.Header),
},
expectError: false,
expectedData: []GraphiteEventsResponse{},
},
{
name: "Invalid JSON response",
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer([]byte("invalid json"))),
Header: make(http.Header),
},
expectError: true,
errorContains: "failed to unmarshal response",
},
{
name: "Empty response body",
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer([]byte(""))),
Header: make(http.Header),
},
expectError: true,
errorContains: "failed to unmarshal response",
},
{
name: "Malformed JSON response",
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBuffer([]byte(`[{"when": 123, "what": }]`))),
Header: make(http.Header),
},
expectError: true,
errorContains: "failed to unmarshal response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, _, err := parseResponse[[]GraphiteEventsResponse](tt.response, false, log.NewNullLogger())
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, result)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedData, *result)
}
})
}
}