Files
grafana/pkg/tsdb/graphite/resource_handler_test.go
Andreas Christou 85e92ce04b Graphite: Backend events endpoint (#110598)
* Add lint rules

* Backend decoupling

- Add standalone files
- Add graphite query type
- Add logger to Service
- Create logger in the ProvideService method
- Use a pointer for the HTTP client provider
- Update logger usage everywhere
- Update tracer type
- Replace simplejson with json
- Add dummy CallResource and CheckHealth methods
- Update tests

* Update ConfigEditor imports

* Update types imports

* Update datasource

- Switch to using semver package
- Update imports

* Update store imports

* Update helper imports and notification creation

* Update context import

* Update version numbers and logic

* Copy array_move from core

* Test updates

* Add required files and update plugin.json

* Update core references and packages

* Remove commented code

* Update wire

* Lint

* Fix import

* Copy null type

* More lint

* Update snapshot

* Refactor backend

- Split query logic into separate file
- Move utils to separate file

* Add health-check logic

- Support backend healthcheck if the FF is enabled

* Remove query import support as unneeded

* Add test

* Add util function for decoding responses

* Add events types

* Add resource handler

* Add events handler and generic resource req handler

* Tests

* Update frontend

- Add types
- Update events function to support backend requests

* Lint and typing

* Lint

* Add tests

* Review

* Review

* Fix packages

* Fix merge issues
2025-09-11 17:08:19 +01:00

268 lines
7.7 KiB
Go

package graphite
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"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
}
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
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
requestBody []byte
expectedStatus int
expectError bool
errorContains string
expectedEvents []GraphiteEventsResponse
}{
{
name: "Success with tags",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://example.com",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
requestBody: func() []byte {
request := GraphiteEventsRequest{From: "now-1h", Until: "now", Tags: "foo"}
body, _ := json.Marshal(request)
return body
}(),
expectedStatus: 200,
expectError: false,
expectedEvents: mockEvents,
},
{
name: "Success without tags",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://example.com",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: mockResp, status: 200}},
},
requestBody: func() []byte {
request := GraphiteEventsRequest{From: "now-1h", Until: "now"}
body, _ := json.Marshal(request)
return body
}(),
expectedStatus: 200,
expectError: false,
expectedEvents: mockEvents,
},
{
name: "Invalid request body",
dsInfo: &datasourceInfo{Id: 1, URL: "http://example.com"},
requestBody: []byte(`{"invalid": json}`),
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "unexpected error",
},
{
name: "Invalid URL",
dsInfo: &datasourceInfo{
Id: 1,
URL: "ht tp://invalid url", // Invalid URL
},
requestBody: func() []byte {
request := GraphiteEventsRequest{From: "now-1h", Until: "now"}
body, _ := json.Marshal(request)
return body
}(),
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "unexpected error",
},
{
name: "HTTP client error",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://example.com",
HTTPClient: &http.Client{Transport: &mockRoundTripper{err: errors.New("network error")}},
},
requestBody: func() []byte {
request := GraphiteEventsRequest{From: "now-1h", Until: "now"}
body, _ := json.Marshal(request)
return body
}(),
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "failed to complete events request",
},
{
name: "Invalid response JSON",
dsInfo: &datasourceInfo{
Id: 1,
URL: "http://example.com",
HTTPClient: &http.Client{Transport: &mockRoundTripper{respBody: []byte("invalid json"), status: 200}},
},
requestBody: func() []byte {
request := GraphiteEventsRequest{From: "now-1h", Until: "now"}
body, _ := json.Marshal(request)
return body
}(),
expectedStatus: http.StatusInternalServerError,
expectError: true,
errorContains: "failed to unmarshal events response",
},
}
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.requestBody)
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 TestHandleResourceReq_Success(t *testing.T) {
mockEvents := []GraphiteEventsResponse{{When: 1234567890, What: "event1"}}
mockResp, _ := json.Marshal(mockEvents)
dsInfo := datasourceInfo{
Id: 1,
URL: "http://example.com",
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 := svc.handleResourceReq(svc.handleEvents)
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 := svc.handleResourceReq(svc.handleEvents)
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://example.com"}
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 := svc.handleResourceReq(nil)
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"])
}