The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go

324 lines
10 KiB

package clientmiddleware
import (
"context"
"encoding/json"
"net/http"
"testing"
"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)
})
})
}