Handle HTML response for functions endpoint - If the endpoint starts with < return an error - Update tests - Catch error in FE and use default functions
1271 lines
36 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|