| 
									
										
										
										
											2023-07-14 01:37:50 +08:00
										 |  |  | package expr | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/json" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/backend" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							|  |  |  | 	"github.com/stretchr/testify/require" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/expr/ml" | 
					
						
							| 
									
										
										
										
											2023-09-25 18:10:47 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/plugins" | 
					
						
							| 
									
										
										
										
											2023-07-14 01:37:50 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/user" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestMLNodeExecute(t *testing.T) { | 
					
						
							|  |  |  | 	timeNow := time.Now() | 
					
						
							|  |  |  | 	expectedOrgID := int64(123) | 
					
						
							|  |  |  | 	timeRange := RelativeTimeRange{ | 
					
						
							|  |  |  | 		From: -10 * time.Hour, | 
					
						
							|  |  |  | 		To:   0, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	request := &Request{ | 
					
						
							|  |  |  | 		Headers: map[string]string{ | 
					
						
							|  |  |  | 			"test": "test", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		Debug:   false, | 
					
						
							|  |  |  | 		OrgId:   expectedOrgID, | 
					
						
							|  |  |  | 		Queries: nil, | 
					
						
							|  |  |  | 		User: &user.SignedInUser{ | 
					
						
							|  |  |  | 			UserID: 1, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	expectedResponse := &backend.CallResourceResponse{ | 
					
						
							|  |  |  | 		Status:  200, | 
					
						
							|  |  |  | 		Headers: nil, | 
					
						
							|  |  |  | 		Body:    []byte("test-response"), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	pluginsClient := &recordingCallResourceHandler{ | 
					
						
							|  |  |  | 		response: expectedResponse, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	pluginCtx := &fakePluginContextProvider{ | 
					
						
							|  |  |  | 		result: map[string]*backend.AppInstanceSettings{ | 
					
						
							|  |  |  | 			mlPluginID: { | 
					
						
							|  |  |  | 				JSONData: json.RawMessage(`{ "initialized": true }`), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	s := &Service{ | 
					
						
							|  |  |  | 		cfg:           nil, | 
					
						
							|  |  |  | 		dataService:   nil, | 
					
						
							|  |  |  | 		pCtxProvider:  pluginCtx, | 
					
						
							|  |  |  | 		features:      nil, | 
					
						
							|  |  |  | 		pluginsClient: pluginsClient, | 
					
						
							|  |  |  | 		tracer:        nil, | 
					
						
							|  |  |  | 		metrics:       newMetrics(nil), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	cmdResponse := data.NewFrame("test", | 
					
						
							|  |  |  | 		data.NewField("Time", nil, []time.Time{time.Unix(1, 0)}), | 
					
						
							|  |  |  | 		data.NewField("Value", nil, []*float64{fp(1)}), | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	cmd := &ml.FakeCommand{ | 
					
						
							|  |  |  | 		Method:  http.MethodPost, | 
					
						
							|  |  |  | 		Path:    "/test/ml", | 
					
						
							|  |  |  | 		Payload: []byte(`{}`), | 
					
						
							|  |  |  | 		Response: &backend.QueryDataResponse{ | 
					
						
							|  |  |  | 			Responses: map[string]backend.DataResponse{ | 
					
						
							|  |  |  | 				"A": { | 
					
						
							|  |  |  | 					Frames: data.Frames{ | 
					
						
							|  |  |  | 						cmdResponse, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					Status: backend.StatusOK, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		Error: nil, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	node := &MLNode{ | 
					
						
							|  |  |  | 		baseNode:  baseNode{}, | 
					
						
							|  |  |  | 		command:   cmd, | 
					
						
							|  |  |  | 		TimeRange: timeRange, | 
					
						
							|  |  |  | 		request:   request, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	result, err := node.Execute(context.Background(), timeNow, nil, s) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 	require.NotNil(t, result) | 
					
						
							|  |  |  | 	require.NotEmpty(t, result.Values) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should get plugin context", func(t *testing.T) { | 
					
						
							|  |  |  | 		require.NotEmpty(t, pluginCtx.recordings) | 
					
						
							|  |  |  | 		require.Equal(t, "Get", pluginCtx.recordings[0].method) | 
					
						
							|  |  |  | 		require.Equal(t, mlPluginID, pluginCtx.recordings[0].params[0]) | 
					
						
							|  |  |  | 		require.Equal(t, request.User, pluginCtx.recordings[0].params[1]) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should call command execute with correct parameters", func(t *testing.T) { | 
					
						
							|  |  |  | 		require.NotEmpty(t, cmd.Recordings) | 
					
						
							|  |  |  | 		rec := cmd.Recordings[0] | 
					
						
							|  |  |  | 		require.Equal(t, timeRange.AbsoluteTime(timeNow).From, rec.From) | 
					
						
							|  |  |  | 		require.Equal(t, timeRange.AbsoluteTime(timeNow).To, rec.To) | 
					
						
							|  |  |  | 		require.NotNil(t, rec.Response) | 
					
						
							|  |  |  | 		require.Equal(t, expectedResponse.Status, rec.Response.Status()) | 
					
						
							|  |  |  | 		require.Equal(t, expectedResponse.Body, rec.Response.Body()) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should call plugin API", func(t *testing.T) { | 
					
						
							|  |  |  | 		require.NotEmpty(t, pluginsClient.recordings) | 
					
						
							|  |  |  | 		req := pluginsClient.recordings[0] | 
					
						
							|  |  |  | 		require.Equal(t, cmd.Payload, req.Body) | 
					
						
							|  |  |  | 		require.Equal(t, cmd.Path, req.Path) | 
					
						
							|  |  |  | 		require.Equal(t, cmd.Method, req.Method) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		require.NotNil(t, req.PluginContext) | 
					
						
							|  |  |  | 		require.Equal(t, mlPluginID, req.PluginContext.PluginID) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		t.Run("should append request headers to API call", func(t *testing.T) { | 
					
						
							|  |  |  | 			for key, value := range request.Headers { | 
					
						
							|  |  |  | 				require.Contains(t, req.Headers, key) | 
					
						
							|  |  |  | 				require.Equal(t, value, req.Headers[key][0]) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should fail if plugin is not installed", func(t *testing.T) { | 
					
						
							|  |  |  | 		s := &Service{ | 
					
						
							|  |  |  | 			cfg:         nil, | 
					
						
							|  |  |  | 			dataService: nil, | 
					
						
							|  |  |  | 			pCtxProvider: &fakePluginContextProvider{ | 
					
						
							| 
									
										
										
										
											2023-09-25 18:10:47 +08:00
										 |  |  | 				errorResult: plugins.ErrPluginNotRegistered, | 
					
						
							| 
									
										
										
										
											2023-07-14 01:37:50 +08:00
										 |  |  | 			}, | 
					
						
							|  |  |  | 			features:      nil, | 
					
						
							|  |  |  | 			pluginsClient: nil, | 
					
						
							|  |  |  | 			tracer:        nil, | 
					
						
							|  |  |  | 			metrics:       nil, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		_, err := node.Execute(context.Background(), timeNow, nil, s) | 
					
						
							|  |  |  | 		require.ErrorIs(t, err, errMLPluginDoesNotExist) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should fail if plugin settings cannot be retrieved", func(t *testing.T) { | 
					
						
							|  |  |  | 		expectedErr := errors.New("test-error") | 
					
						
							|  |  |  | 		s := &Service{ | 
					
						
							|  |  |  | 			cfg:         nil, | 
					
						
							|  |  |  | 			dataService: nil, | 
					
						
							|  |  |  | 			pCtxProvider: &fakePluginContextProvider{ | 
					
						
							|  |  |  | 				errorResult: expectedErr, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			features:      nil, | 
					
						
							|  |  |  | 			pluginsClient: nil, | 
					
						
							|  |  |  | 			tracer:        nil, | 
					
						
							|  |  |  | 			metrics:       nil, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		_, err := node.Execute(context.Background(), timeNow, nil, s) | 
					
						
							|  |  |  | 		require.ErrorIs(t, err, expectedErr) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should fail if plugin is not initialized", func(t *testing.T) { | 
					
						
							|  |  |  | 		s := &Service{ | 
					
						
							|  |  |  | 			cfg:         nil, | 
					
						
							|  |  |  | 			dataService: nil, | 
					
						
							|  |  |  | 			pCtxProvider: &fakePluginContextProvider{ | 
					
						
							|  |  |  | 				result: map[string]*backend.AppInstanceSettings{ | 
					
						
							|  |  |  | 					mlPluginID: { | 
					
						
							|  |  |  | 						JSONData: json.RawMessage(`{}`), | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			features:      nil, | 
					
						
							|  |  |  | 			pluginsClient: nil, | 
					
						
							|  |  |  | 			tracer:        nil, | 
					
						
							|  |  |  | 			metrics:       nil, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		_, err := node.Execute(context.Background(), timeNow, nil, s) | 
					
						
							|  |  |  | 		require.ErrorIs(t, err, errMLPluginDoesNotExist) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	t.Run("should return QueryError if command failed", func(t *testing.T) { | 
					
						
							|  |  |  | 		s := &Service{ | 
					
						
							|  |  |  | 			cfg:           nil, | 
					
						
							|  |  |  | 			dataService:   nil, | 
					
						
							|  |  |  | 			pCtxProvider:  pluginCtx, | 
					
						
							|  |  |  | 			features:      nil, | 
					
						
							|  |  |  | 			pluginsClient: pluginsClient, | 
					
						
							|  |  |  | 			tracer:        nil, | 
					
						
							|  |  |  | 			metrics:       newMetrics(nil), | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		cmd := &ml.FakeCommand{ | 
					
						
							|  |  |  | 			Error: errors.New("failed to execute command"), | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		node := &MLNode{ | 
					
						
							|  |  |  | 			baseNode:  baseNode{}, | 
					
						
							|  |  |  | 			command:   cmd, | 
					
						
							|  |  |  | 			TimeRange: timeRange, | 
					
						
							|  |  |  | 			request:   request, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		_, err := node.Execute(context.Background(), timeNow, nil, s) | 
					
						
							| 
									
										
										
										
											2023-07-21 18:38:29 +08:00
										 |  |  | 		require.IsType(t, err, MakeQueryError("A", "", expectedError{})) | 
					
						
							| 
									
										
										
										
											2023-07-14 01:37:50 +08:00
										 |  |  | 		require.ErrorIs(t, err, cmd.Error) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } |