Query library: requiresDevMode dummy backend (#56466)

* query library - dummy backend

* fix tests

* dont explicitly marshall backend dataresponse

* skip integration tests

* null check for tests

* added query library to codeowners

* null check for tests

* lint
This commit is contained in:
Artur Wierzbicki
2022-10-07 22:31:45 +04:00
committed by GitHub
parent 23e04c0f9c
commit bf264d2f76
24 changed files with 1292 additions and 13 deletions
@@ -0,0 +1,284 @@
package querylibrary_tests
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
)
type queryLibraryAPIClient struct {
token string
url string
user *user.SignedInUser
sqlStore *sqlstore.SQLStore
}
func newQueryLibraryAPIClient(token string, baseUrl string, user *user.SignedInUser, sqlStore *sqlstore.SQLStore) *queryLibraryAPIClient {
return &queryLibraryAPIClient{
token: token,
url: baseUrl,
user: user,
sqlStore: sqlStore,
}
}
func (q *queryLibraryAPIClient) update(ctx context.Context, query *querylibrary.Query) error {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(query)
if err != nil {
return err
}
url := fmt.Sprintf("%s/query-library", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
return err
}
func (q *queryLibraryAPIClient) delete(ctx context.Context, uid string) error {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "DELETE", url, bytes.NewBuffer([]byte("")))
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
return err
}
func (q *queryLibraryAPIClient) get(ctx context.Context, uid string) (*querylibrary.Query, error) {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
query := make([]*querylibrary.Query, 0)
err = json.Unmarshal(b, &query)
if len(query) > 0 {
return query[0], err
}
return nil, err
}
type querySearchInfo struct {
kind string
uid string
name string
dsUIDs []string
location string
}
func (q *queryLibraryAPIClient) search(ctx context.Context, options querylibrary.QuerySearchOptions) ([]*querySearchInfo, error) {
return q.searchRetry(ctx, options, 1)
}
func (q *queryLibraryAPIClient) searchRetry(ctx context.Context, options querylibrary.QuerySearchOptions, attempt int) ([]*querySearchInfo, error) {
if attempt >= 3 {
return nil, errors.New("max attempts")
}
url := fmt.Sprintf("%s/search-v2", q.url)
text := "*"
if options.Query != "" {
text = options.Query
}
searchReq := map[string]interface{}{
"query": text,
"sort": "name_sort",
"kind": []string{"query"},
"limit": 50,
}
searchReqJson, err := simplejson.NewFromAny(searchReq).MarshalJSON()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(searchReqJson))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
r := &backend.DataResponse{}
err = json.Unmarshal(b, r)
if len(r.Frames) != 1 {
return nil, fmt.Errorf("expected a single frame, received %s", string(b))
}
frame := r.Frames[0]
if frame.Name == "Loading" {
time.Sleep(100 * time.Millisecond)
return q.searchRetry(ctx, options, attempt+1)
}
res := make([]*querySearchInfo, 0)
frameLen, _ := frame.RowLen()
for i := 0; i < frameLen; i++ {
fKind, _ := frame.FieldByName("kind")
fUid, _ := frame.FieldByName("uid")
fName, _ := frame.FieldByName("name")
dsUID, _ := frame.FieldByName("ds_uid")
fLocation, _ := frame.FieldByName("location")
rawValue, ok := dsUID.At(i).(json.RawMessage)
if !ok || rawValue == nil {
return nil, errors.New("invalid ds_uid field")
}
jsonValue, err := rawValue.MarshalJSON()
if err != nil {
return nil, err
}
var uids []string
err = json.Unmarshal(jsonValue, &uids)
if err != nil {
return nil, err
}
res = append(res, &querySearchInfo{
kind: fKind.At(i).(string),
uid: fUid.At(i).(string),
name: fName.At(i).(string),
dsUIDs: uids,
location: fLocation.At(i).(string),
})
}
return res, err
}
func (q *queryLibraryAPIClient) getDashboard(ctx context.Context, uid string) (*dtos.DashboardFullWithMeta, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/dashboards/uid/%s", q.url, uid), bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
res := &dtos.DashboardFullWithMeta{}
err = json.Unmarshal(b, res)
if err != nil {
return nil, err
}
return res, nil
}
func (q *queryLibraryAPIClient) createDashboard(ctx context.Context, dash *simplejson.Json) (string, error) {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
dashMap, err := dash.Map()
if err != nil {
return "", err
}
err = enc.Encode(dashMap)
if err != nil {
return "", err
}
url := fmt.Sprintf("%s/dashboards/db", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
if err != nil {
return "", err
}
jsonResp, err := simplejson.NewFromReader(resp.Body)
if err != nil {
return "", err
}
return jsonResp.Get("uid").MustString(), nil
}
+72
View File
@@ -0,0 +1,72 @@
package querylibrary_tests
import (
"fmt"
"testing"
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
saAPI "github.com/grafana/grafana/pkg/services/serviceaccounts/api"
saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/stretchr/testify/require"
)
func createServiceAccountAdminToken(t *testing.T, name string, env *server.TestEnv) (string, *user.SignedInUser) {
t.Helper()
account := saTests.SetupUserServiceAccount(t, env.SQLStore, saTests.TestUser{
Name: name,
Role: string(org.RoleAdmin),
Login: name,
IsServiceAccount: true,
OrgID: 1,
})
keyGen, err := apikeygenprefix.New(saAPI.ServiceID)
require.NoError(t, err)
_ = saTests.SetupApiKey(t, env.SQLStore, saTests.TestApiKey{
Name: name,
Role: org.RoleAdmin,
OrgId: account.OrgID,
Key: keyGen.HashedKey,
ServiceAccountID: &account.ID,
})
return keyGen.ClientSecret, &user.SignedInUser{
UserID: account.ID,
Email: account.Email,
Name: account.Name,
Login: account.Login,
OrgID: account.OrgID,
}
}
type testContext struct {
authToken string
client *queryLibraryAPIClient
user *user.SignedInUser
}
func createTestContext(t *testing.T) testContext {
t.Helper()
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagQueryLibrary},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
authToken, serviceAccountUser := createServiceAccountAdminToken(t, "query-library", env)
client := newQueryLibraryAPIClient(authToken, fmt.Sprintf("http://%s/api", grafanaListedAddr), serviceAccountUser, env.SQLStore)
return testContext{
authToken: authToken,
client: client,
user: serviceAccountUser,
}
}
@@ -0,0 +1,289 @@
package querylibrary_tests
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
func TestCreateAndDelete(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
info := search[0]
require.Equal(t, "query", info.kind)
require.Equal(t, "first query", info.name)
require.Equal(t, "General", info.location)
require.Equal(t, []string{grafanads.DatasourceUID, grafanads.DatasourceUID}, info.dsUIDs)
err = testCtx.client.delete(ctx, info.uid)
require.NoError(t, err)
search, err = testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 0)
query, err := testCtx.client.get(ctx, info.uid)
require.NoError(t, err)
require.Nil(t, query)
}
func createQuery(t *testing.T, ctx context.Context, testCtx testContext) string {
t.Helper()
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
return search[0].uid
}
func TestDashboardGetWithLatestSavedQueries(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
queryUID := createQuery(t, ctx, testCtx)
dashUID, err := testCtx.client.createDashboard(ctx, simplejson.NewFromAny(map[string]interface{}{
"dashboard": map[string]interface{}{
"title": "my-new-dashboard",
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"savedQueryLink": map[string]interface{}{
"ref": map[string]string{
"uid": queryUID,
},
},
},
},
},
"folderId": 0,
"message": "",
"overwrite": true,
}))
require.NoError(t, err)
dashboard, err := testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err := dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel := simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
// update, expect changes when getting dashboards
err = testCtx.client.update(ctx, &querylibrary.Query{
UID: queryUID,
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
dashboard, err = testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err = dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel = simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": "-- Mixed --",
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
}