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
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
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"])
|
||||
}
|
||||
Reference in New Issue
Block a user