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/store/entity/tests/server_integration_test.go

566 lines
17 KiB

package entity_server_tests
import (
_ "embed"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/store/entity"
)
var (
//go:embed testdata/dashboard-with-tags-b-g.json
dashboardWithTagsBlueGreen string
//go:embed testdata/dashboard-with-tags-r-g.json
dashboardWithTagsRedGreen string
)
type rawEntityMatcher struct {
key string
createdRange []time.Time
updatedRange []time.Time
createdBy string
updatedBy string
body []byte
version int64
}
type objectVersionMatcher struct {
updatedRange []time.Time
updatedBy string
version int64
etag *string
comment *string
}
func timestampInRange(ts int64, tsRange []time.Time) bool {
low := tsRange[0].UnixMilli() - 1
high := tsRange[1].UnixMilli() + 1
return ts >= low && ts <= high
}
func requireEntityMatch(t *testing.T, obj *entity.Entity, m rawEntityMatcher) {
t.Helper()
require.NotNil(t, obj)
mismatches := ""
if m.key != "" && m.key != obj.Key {
mismatches += fmt.Sprintf("expected key: %s, actual: %s\n", m.key, obj.Key)
}
if len(m.createdRange) == 2 && !timestampInRange(obj.CreatedAt, m.createdRange) {
mismatches += fmt.Sprintf("expected Created range: [from %s to %s], actual created: %s\n", m.createdRange[0], m.createdRange[1], time.UnixMilli(obj.CreatedAt))
}
if len(m.updatedRange) == 2 && !timestampInRange(obj.UpdatedAt, m.updatedRange) {
mismatches += fmt.Sprintf("expected Updated range: [from %s to %s], actual updated: %s\n", m.updatedRange[0], m.updatedRange[1], time.UnixMilli(obj.UpdatedAt))
}
if m.createdBy != "" && m.createdBy != obj.CreatedBy {
mismatches += fmt.Sprintf("createdBy: expected: '%s', found: '%s'\n", m.createdBy, obj.CreatedBy)
}
if m.updatedBy != "" && m.updatedBy != obj.UpdatedBy {
mismatches += fmt.Sprintf("updatedBy: expected: '%s', found: '%s'\n", m.updatedBy, obj.UpdatedBy)
}
if len(m.body) > 0 {
if json.Valid(m.body) {
require.JSONEq(t, string(m.body), string(obj.Body), "expecting same body")
} else if !reflect.DeepEqual(m.body, obj.Body) {
mismatches += fmt.Sprintf("expected body len: %d, actual body len: %d\n", len(m.body), len(obj.Body))
}
}
if m.version != 0 && m.version != obj.ResourceVersion {
mismatches += fmt.Sprintf("expected version: %d, actual version: %d\n", m.version, obj.ResourceVersion)
}
require.True(t, len(mismatches) == 0, mismatches)
}
func requireVersionMatch(t *testing.T, obj *entity.Entity, m objectVersionMatcher) {
t.Helper()
mismatches := ""
if m.etag != nil && *m.etag != obj.ETag {
mismatches += fmt.Sprintf("expected etag: %s, actual etag: %s\n", *m.etag, obj.ETag)
}
if len(m.updatedRange) == 2 && !timestampInRange(obj.UpdatedAt, m.updatedRange) {
mismatches += fmt.Sprintf("expected updatedRange range: [from %s to %s], actual updated: %s\n", m.updatedRange[0], m.updatedRange[1], time.UnixMilli(obj.UpdatedAt))
}
if m.updatedBy != "" && m.updatedBy != obj.UpdatedBy {
mismatches += fmt.Sprintf("updatedBy: expected: '%s', found: '%s'\n", m.updatedBy, obj.UpdatedBy)
}
if m.version != 0 && m.version != obj.ResourceVersion {
mismatches += fmt.Sprintf("expected version: %d, actual version: %d\n", m.version, obj.ResourceVersion)
}
require.True(t, len(mismatches) == 0, mismatches)
}
func TestIntegrationEntityServer(t *testing.T) {
// TODO figure out why this still runs into sqlite database locked error
if true {
t.Skip("skipping integration test")
}
if testing.Short() {
t.Skip("skipping integration test")
}
testCtx := createTestContext(t)
ctx := identity.WithRequester(testCtx.ctx, testCtx.user)
fakeUser := testCtx.user.GetUID().String()
firstVersion := int64(0)
group := "test.grafana.app"
resource := "jsonobjs"
resource2 := "playlists"
namespace := "default"
name := "my-test-entity"
testKey := "/" + group + "/" + resource + "/" + namespace + "/" + name
testKey2 := "/" + group + "/" + resource2 + "/" + namespace + "/" + name
body := []byte("{\"name\":\"John\"}")
t.Run("should not retrieve non-existent objects", func(t *testing.T) {
resp, err := testCtx.client.Read(ctx, &entity.ReadEntityRequest{
Key: testKey,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Empty(t, resp.Key)
})
t.Run("should be able to read persisted objects", func(t *testing.T) {
before := time.Now()
createReq := &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey,
Group: group,
Resource: resource,
Namespace: namespace,
Name: name,
Body: body,
Message: "first entity!",
},
}
createResp, err := testCtx.client.Create(ctx, createReq)
require.NoError(t, err)
// clean up in case test fails
t.Cleanup(func() {
_, _ = testCtx.client.Delete(ctx, &entity.DeleteEntityRequest{
Key: testKey,
})
})
versionMatcher := objectVersionMatcher{
// updatedRange: []time.Time{before, time.Now()},
// updatedBy: fakeUser,
version: firstVersion,
comment: &createReq.Entity.Message,
}
requireVersionMatch(t, createResp.Entity, versionMatcher)
readResp, err := testCtx.client.Read(ctx, &entity.ReadEntityRequest{
Key: testKey,
ResourceVersion: 0,
WithBody: true,
})
require.NoError(t, err)
require.NotNil(t, readResp)
require.Equal(t, testKey, readResp.Key)
require.Equal(t, namespace, readResp.Namespace) // orgId becomes the tenant id when not set
require.Equal(t, resource, readResp.Resource)
require.Equal(t, name, readResp.Name)
objectMatcher := rawEntityMatcher{
key: testKey,
createdRange: []time.Time{before, time.Now()},
// updatedRange: []time.Time{before, time.Now()},
createdBy: fakeUser,
// updatedBy: fakeUser,
body: body,
version: firstVersion,
}
requireEntityMatch(t, readResp, objectMatcher)
deleteResp, err := testCtx.client.Delete(ctx, &entity.DeleteEntityRequest{
Key: testKey,
PreviousVersion: readResp.ResourceVersion,
})
require.NoError(t, err)
require.Equal(t, deleteResp.Status, entity.DeleteEntityResponse_DELETED)
readRespAfterDelete, err := testCtx.client.Read(ctx, &entity.ReadEntityRequest{
Key: testKey,
ResourceVersion: 0,
WithBody: true,
})
require.NoError(t, err)
require.Empty(t, readRespAfterDelete.Key)
})
t.Run("should be able to update an object", func(t *testing.T) {
before := time.Now()
createReq := &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey,
Group: group,
Resource: resource,
Namespace: namespace,
Name: name,
Body: body,
Message: "first entity!",
},
}
createResp, err := testCtx.client.Create(ctx, createReq)
require.NoError(t, err)
// clean up in case test fails
t.Cleanup(func() {
_, _ = testCtx.client.Delete(ctx, &entity.DeleteEntityRequest{
Key: testKey,
})
})
require.Equal(t, entity.CreateEntityResponse_CREATED, createResp.Status)
body2 := []byte("{\"name\":\"John2\"}")
updateReq := &entity.UpdateEntityRequest{
Entity: &entity.Entity{
Key: testKey,
Body: body2,
Message: "update1",
},
}
updateResp, err := testCtx.client.Update(ctx, updateReq)
require.NoError(t, err)
require.NotEqual(t, createResp.Entity.ResourceVersion, updateResp.Entity.ResourceVersion)
// Duplicate write (no change)
/*
writeDupRsp, err := testCtx.client.Update(ctx, updateReq)
require.NoError(t, err)
require.Nil(t, writeDupRsp.Error)
require.Equal(t, entity.UpdateEntityResponse_UNCHANGED, writeDupRsp.Status)
require.Equal(t, updateResp.Entity.ResourceVersion, writeDupRsp.Entity.ResourceVersion)
require.Equal(t, updateResp.Entity.ETag, writeDupRsp.Entity.ETag)
*/
body3 := []byte("{\"name\":\"John3\"}")
writeReq3 := &entity.UpdateEntityRequest{
Entity: &entity.Entity{
Key: testKey,
Body: body3,
Message: "update3",
},
}
writeResp3, err := testCtx.client.Update(ctx, writeReq3)
require.NoError(t, err)
require.Equal(t, entity.UpdateEntityResponse_UPDATED, writeResp3.Status)
require.NotEqual(t, writeResp3.Entity.ResourceVersion, updateResp.Entity.ResourceVersion)
latestMatcher := rawEntityMatcher{
key: testKey,
createdRange: []time.Time{before, time.Now()},
updatedRange: []time.Time{before, time.Now()},
createdBy: fakeUser,
updatedBy: fakeUser,
body: body3,
version: writeResp3.Entity.ResourceVersion,
}
readRespLatest, err := testCtx.client.Read(ctx, &entity.ReadEntityRequest{
Key: testKey,
ResourceVersion: 0, // latest
WithBody: true,
})
require.NoError(t, err)
requireEntityMatch(t, readRespLatest, latestMatcher)
readRespFirstVer, err := testCtx.client.Read(ctx, &entity.ReadEntityRequest{
Key: testKey,
ResourceVersion: createResp.Entity.ResourceVersion,
WithBody: true,
})
require.NoError(t, err)
require.NotNil(t, readRespFirstVer)
requireEntityMatch(t, readRespFirstVer, rawEntityMatcher{
key: testKey,
createdRange: []time.Time{before, time.Now()},
createdBy: fakeUser,
body: body,
version: 0,
})
history, err := testCtx.client.History(ctx, &entity.EntityHistoryRequest{
Key: testKey,
})
require.NoError(t, err)
require.Equal(t, []*entity.Entity{
writeResp3.Entity,
updateResp.Entity,
createResp.Entity,
}, history.Versions)
deleteResp, err := testCtx.client.Delete(ctx, &entity.DeleteEntityRequest{
Key: testKey,
PreviousVersion: writeResp3.Entity.ResourceVersion,
})
require.NoError(t, err)
require.Equal(t, deleteResp.Status, entity.DeleteEntityResponse_DELETED)
})
t.Run("should be able to list objects", func(t *testing.T) {
w1, err := testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey + "1",
Body: body,
},
})
require.NoError(t, err)
w2, err := testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey + "2",
Body: body,
},
})
require.NoError(t, err)
w3, err := testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey2 + "3",
Body: body,
},
})
require.NoError(t, err)
w4, err := testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: testKey2 + "4",
Body: body,
},
})
require.NoError(t, err)
resp, err := testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource, resource2},
WithBody: false,
})
require.NoError(t, err)
require.NotNil(t, resp)
names := make([]string, 0, len(resp.Results))
kinds := make([]string, 0, len(resp.Results))
version := make([]int64, 0, len(resp.Results))
for _, res := range resp.Results {
names = append(names, res.Name)
kinds = append(kinds, res.Resource)
version = append(version, res.ResourceVersion)
}
// default sort is by guid, so we ignore order
require.ElementsMatch(t, []string{"my-test-entity1", "my-test-entity2", "my-test-entity3", "my-test-entity4"}, names)
require.ElementsMatch(t, []string{"jsonobjs", "jsonobjs", "playlists", "playlists"}, kinds)
require.ElementsMatch(t, []int64{
w1.Entity.ResourceVersion,
w2.Entity.ResourceVersion,
w3.Entity.ResourceVersion,
w4.Entity.ResourceVersion,
}, version)
// sorted by name
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource, resource2},
WithBody: false,
Sort: []string{"name"},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, 4, len(resp.Results))
require.Equal(t, "my-test-entity1", resp.Results[0].Name)
require.Equal(t, "my-test-entity2", resp.Results[1].Name)
require.Equal(t, "my-test-entity3", resp.Results[2].Name)
require.Equal(t, "my-test-entity4", resp.Results[3].Name)
require.Equal(t, "jsonobjs", resp.Results[0].Resource)
require.Equal(t, "jsonobjs", resp.Results[1].Resource)
require.Equal(t, "playlists", resp.Results[2].Resource)
require.Equal(t, "playlists", resp.Results[3].Resource)
// sorted by name desc
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource, resource2},
WithBody: false,
Sort: []string{"name_desc"},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, 4, len(resp.Results))
require.Equal(t, "my-test-entity1", resp.Results[3].Name)
require.Equal(t, "my-test-entity2", resp.Results[2].Name)
require.Equal(t, "my-test-entity3", resp.Results[1].Name)
require.Equal(t, "my-test-entity4", resp.Results[0].Name)
require.Equal(t, "jsonobjs", resp.Results[3].Resource)
require.Equal(t, "jsonobjs", resp.Results[2].Resource)
require.Equal(t, "playlists", resp.Results[1].Resource)
require.Equal(t, "playlists", resp.Results[0].Resource)
// with limit
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource, resource2},
WithBody: false,
Limit: 2,
Sort: []string{"name"},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, 2, len(resp.Results))
require.Equal(t, "my-test-entity1", resp.Results[0].Name)
require.Equal(t, "my-test-entity2", resp.Results[1].Name)
// with limit & continue
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource, resource2},
WithBody: false,
Limit: 2,
NextPageToken: resp.NextPageToken,
Sort: []string{"name"},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, 2, len(resp.Results))
require.Equal(t, "my-test-entity3", resp.Results[0].Name)
require.Equal(t, "my-test-entity4", resp.Results[1].Name)
// Again with only one kind
respKind1, err := testCtx.client.List(ctx, &entity.EntityListRequest{
Resource: []string{resource},
Sort: []string{"name"},
})
require.NoError(t, err)
names = make([]string, 0, len(respKind1.Results))
kinds = make([]string, 0, len(respKind1.Results))
version = make([]int64, 0, len(respKind1.Results))
for _, res := range respKind1.Results {
names = append(names, res.Name)
kinds = append(kinds, res.Resource)
version = append(version, res.ResourceVersion)
}
require.Equal(t, []string{"my-test-entity1", "my-test-entity2"}, names)
require.Equal(t, []string{"jsonobjs", "jsonobjs"}, kinds)
require.Equal(t, []int64{
w1.Entity.ResourceVersion,
w2.Entity.ResourceVersion,
}, version)
})
t.Run("should be able to filter objects based on their labels", func(t *testing.T) {
_, err := testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: "/dashboards.grafana.app/dashboards/default/blue-green",
Body: []byte(dashboardWithTagsBlueGreen),
Labels: map[string]string{
"blue": "",
"green": "",
},
},
})
require.NoError(t, err)
_, err = testCtx.client.Create(ctx, &entity.CreateEntityRequest{
Entity: &entity.Entity{
Key: "/dashboards.grafana.app/dashboards/default/red-green",
Body: []byte(dashboardWithTagsRedGreen),
Labels: map[string]string{
"red": "",
"green": "",
},
},
})
require.NoError(t, err)
resp, err := testCtx.client.List(ctx, &entity.EntityListRequest{
Key: []string{"/dashboards.grafana.app/dashboards/default"},
WithBody: false,
Labels: map[string]string{
"red": "",
},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Results, 1)
require.Equal(t, resp.Results[0].Name, "red-green")
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Key: []string{"/dashboards.grafana.app/dashboards/default"},
WithBody: false,
Labels: map[string]string{
"red": "",
"green": "",
},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Results, 1)
require.Equal(t, resp.Results[0].Name, "red-green")
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Key: []string{"/dashboards.grafana.app/dashboards/default"},
WithBody: false,
Labels: map[string]string{
"red": "invalid",
},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Results, 0)
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Key: []string{"/dashboards.grafana.app/dashboards/default"},
WithBody: false,
Labels: map[string]string{
"green": "",
},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Results, 2)
resp, err = testCtx.client.List(ctx, &entity.EntityListRequest{
Key: []string{"/dashboards.grafana.app/dashboards/default"},
WithBody: false,
Labels: map[string]string{
"yellow": "",
},
})
require.NoError(t, err)
require.NotNil(t, resp)
require.Len(t, resp.Results, 0)
})
}