Files
grafana/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go
Bruno acd9838701 caching middleware: handle req.PluginContext.DataSourceInstanceSettings being nil (#111258)
* caching middleware: handle req.PluginContext.DataSourceInstanceSettings being nil

* add test case
2025-09-18 09:40:04 -03:00

411 lines
12 KiB
Go

package clientmiddleware
import (
"context"
"encoding/json"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/handlertest"
"github.com/grafana/grafana/pkg/services/caching"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCachingMiddleware(t *testing.T) {
t.Run("When QueryData is called", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/query", nil)
require.NoError(t, err)
cs := caching.NewFakeOSSCachingService()
cdt := handlertest.NewHandlerMiddlewareTest(t,
WithReqContext(req, &user.SignedInUser{}),
handlertest.WithMiddlewares(NewCachingMiddleware(cs)),
)
jsonDataMap := map[string]any{}
jsonDataBytes, err := json.Marshal(&jsonDataMap)
require.NoError(t, err)
pluginCtx := backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
JSONData: jsonDataBytes,
},
}
// Populated by clienttest.WithReqContext
reqCtx := contexthandler.FromContext(req.Context())
require.NotNil(t, reqCtx)
qdr := &backend.QueryDataRequest{
PluginContext: pluginCtx,
}
// Track whether the update cache fn was called, depending on what the response headers are in the cache request
var updateCacheCalled bool
dataResponse := caching.CachedQueryDataResponse{
Response: &backend.QueryDataResponse{},
UpdateCacheFn: func(ctx context.Context, qdr *backend.QueryDataResponse) {
updateCacheCalled = true
},
}
t.Run("If cache returns a hit, no queries are issued", func(t *testing.T) {
t.Cleanup(func() {
updateCacheCalled = false
cs.Reset()
})
cs.ReturnHit = true
cs.ReturnQueryResponse = dataResponse
resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleQueryRequest", 1)
// Equals the mocked response
assert.NotNil(t, resp)
assert.Equal(t, dataResponse.Response, resp)
// Cache was not updated by the middleware
assert.False(t, updateCacheCalled)
})
t.Run("If cache returns a miss, queries are issued and the update cache function is called", func(t *testing.T) {
origShouldCacheQuery := shouldCacheQuery
var shouldCacheQueryCalled bool
shouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
shouldCacheQueryCalled = true
return true
}
t.Cleanup(func() {
updateCacheCalled = false
shouldCacheQueryCalled = false
shouldCacheQuery = origShouldCacheQuery
cs.Reset()
})
cs.ReturnHit = false
cs.ReturnQueryResponse = dataResponse
resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleQueryRequest", 1)
// Equals nil (returned by the decorator test)
assert.Nil(t, resp)
// Since it was a miss, the middleware called the update func
assert.True(t, updateCacheCalled)
// Since the feature flag was not set, the middleware did not call shouldCacheQuery
assert.False(t, shouldCacheQueryCalled)
})
t.Run("with async queries", func(t *testing.T) {
asyncCdt := handlertest.NewHandlerMiddlewareTest(t,
WithReqContext(req, &user.SignedInUser{}),
handlertest.WithMiddlewares(
NewCachingMiddlewareWithFeatureManager(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching))),
)
t.Run("If shoudCacheQuery returns true update cache function is called", func(t *testing.T) {
origShouldCacheQuery := shouldCacheQuery
var shouldCacheQueryCalled bool
shouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
shouldCacheQueryCalled = true
return true
}
t.Cleanup(func() {
updateCacheCalled = false
shouldCacheQueryCalled = false
shouldCacheQuery = origShouldCacheQuery
cs.Reset()
})
cs.ReturnHit = false
cs.ReturnQueryResponse = dataResponse
resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleQueryRequest", 1)
// Equals nil (returned by the decorator test)
assert.Nil(t, resp)
// Since it was a miss, the middleware called the update func
assert.True(t, updateCacheCalled)
// Since the feature flag set, the middleware called shouldCacheQuery
assert.True(t, shouldCacheQueryCalled)
})
t.Run("If shoudCacheQuery returns false update cache function is not called", func(t *testing.T) {
origShouldCacheQuery := shouldCacheQuery
var shouldCacheQueryCalled bool
shouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
shouldCacheQueryCalled = true
return false
}
t.Cleanup(func() {
updateCacheCalled = false
shouldCacheQueryCalled = false
shouldCacheQuery = origShouldCacheQuery
cs.Reset()
})
cs.ReturnHit = false
cs.ReturnQueryResponse = dataResponse
resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleQueryRequest", 1)
// Equals nil (returned by the decorator test)
assert.Nil(t, resp)
// Since it was a miss, the middleware called the update func
assert.False(t, updateCacheCalled)
// Since the feature flag set, the middleware called shouldCacheQuery
assert.True(t, shouldCacheQueryCalled)
})
})
})
t.Run("When CallResource is called", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/resource/blah", nil)
require.NoError(t, err)
// This is the response returned by the HandleResourceRequest call
// Track whether the update cache fn was called, depending on what the response headers are in the cache request
var updateCacheCalled bool
dataResponse := caching.CachedResourceDataResponse{
Response: &backend.CallResourceResponse{
Status: 200,
Body: []byte("bogus"),
},
UpdateCacheFn: func(ctx context.Context, rdr *backend.CallResourceResponse) {
updateCacheCalled = true
},
}
// This is the response sent via the passed-in sender when there is a cache miss
simulatedPluginResponse := &backend.CallResourceResponse{
Status: 201,
Body: []byte("bogus"),
}
cs := caching.NewFakeOSSCachingService()
cdt := handlertest.NewHandlerMiddlewareTest(t,
WithReqContext(req, &user.SignedInUser{}),
handlertest.WithMiddlewares(NewCachingMiddleware(cs)),
handlertest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}),
)
jsonDataMap := map[string]any{}
jsonDataBytes, err := json.Marshal(&jsonDataMap)
require.NoError(t, err)
pluginCtx := backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
JSONData: jsonDataBytes,
},
}
// Populated by clienttest.WithReqContext
reqCtx := contexthandler.FromContext(req.Context())
require.NotNil(t, reqCtx)
crr := &backend.CallResourceRequest{
PluginContext: pluginCtx,
}
var sentResponse *backend.CallResourceResponse
var storeOneResponseCallResourceSender = backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
sentResponse = res
return nil
})
t.Run("If cache returns a hit, no resource call is issued", func(t *testing.T) {
t.Cleanup(func() {
sentResponse = nil
cs.Reset()
})
cs.ReturnHit = true
cs.ReturnResourceResponse = dataResponse
err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleResourceRequest", 1)
// The mocked cached response was sent
assert.NotNil(t, sentResponse)
assert.Equal(t, dataResponse.Response, sentResponse)
// Cache was not updated by the middleware
assert.False(t, updateCacheCalled)
})
t.Run("If cache returns a miss, resource call is issued and the update cache function is called", func(t *testing.T) {
t.Cleanup(func() {
sentResponse = nil
cs.Reset()
})
cs.ReturnHit = false
cs.ReturnResourceResponse = dataResponse
err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender)
assert.NoError(t, err)
// Cache service is called once
cs.AssertCalls(t, "HandleResourceRequest", 1)
// Simulated plugin response was sent
assert.NotNil(t, sentResponse)
assert.Equal(t, simulatedPluginResponse, sentResponse)
// Since it was a miss, the middleware called the update func
assert.True(t, updateCacheCalled)
})
})
t.Run("When RequestContext is nil", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/doesnt/matter", nil)
require.NoError(t, err)
cs := caching.NewFakeOSSCachingService()
cdt := handlertest.NewHandlerMiddlewareTest(t,
// Skip the request context in this case
handlertest.WithMiddlewares(NewCachingMiddleware(cs)),
)
reqCtx := contexthandler.FromContext(req.Context())
require.Nil(t, reqCtx)
jsonDataMap := map[string]any{}
jsonDataBytes, err := json.Marshal(&jsonDataMap)
require.NoError(t, err)
pluginCtx := backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
JSONData: jsonDataBytes,
},
}
t.Run("Query caching is skipped", func(t *testing.T) {
t.Cleanup(func() {
cs.Reset()
})
qdr := &backend.QueryDataRequest{
PluginContext: pluginCtx,
}
resp, err := cdt.MiddlewareHandler.QueryData(context.Background(), qdr)
assert.NoError(t, err)
// Cache service is never called
cs.AssertCalls(t, "HandleQueryRequest", 0)
// Equals nil (returned by the decorator test)
assert.Nil(t, resp)
})
t.Run("Resource caching is skipped", func(t *testing.T) {
t.Cleanup(func() {
cs.Reset()
})
crr := &backend.CallResourceRequest{
PluginContext: pluginCtx,
}
err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, nopCallResourceSender)
assert.NoError(t, err)
// Cache service is never called
cs.AssertCalls(t, "HandleResourceRequest", 0)
})
})
}
func TestRequestDeduplicationMiddleware(t *testing.T) {
t.Parallel()
t.Run("deduplicates requests issuing the same query", func(t *testing.T) {
t.Parallel()
handler := newMockMiddlewareHandler()
middleware := newRequestDeduplicationMiddleware(nil, handler)
req := backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
UID: "uid",
},
},
}
wg := &sync.WaitGroup{}
wg.Add(2)
for range 2 {
go func() {
defer wg.Done()
resp, err := middleware.QueryData(t.Context(), &req)
require.NoError(t, err)
require.Equal(t, &backend.QueryDataResponse{}, resp)
}()
}
wg.Wait()
require.EqualValues(t, 1, handler.QueryDataCalls)
})
t.Run("requests where DataSourceInstanceSettings is nil bypass request deduplication", func(t *testing.T) {
t.Parallel()
handler := newMockMiddlewareHandler()
middleware := newRequestDeduplicationMiddleware(nil, handler)
{
req := backend.QueryDataRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: nil,
},
}
resp, err := middleware.QueryData(t.Context(), &req)
require.NoError(t, err)
require.Empty(t, resp)
}
{
req := backend.CallResourceRequest{
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: nil,
},
}
require.NoError(t, middleware.CallResource(t.Context(), &req, nil))
}
})
}
type mockMiddlewareHandler struct {
backend.BaseHandler
QueryDataCalls int32
}
func newMockMiddlewareHandler() *mockMiddlewareHandler {
return &mockMiddlewareHandler{}
}
func (m *mockMiddlewareHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
atomic.AddInt32(&m.QueryDataCalls, 1)
time.Sleep(10 * time.Millisecond)
return &backend.QueryDataResponse{}, nil
}
func (m *mockMiddlewareHandler) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return nil
}