Files
grafana/pkg/services/store/entity/tests/server_integration_test.go
2024-06-12 19:39:34 +03:00

567 lines
17 KiB
Go

package entity_server_tests
import (
_ "embed"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/appcontext"
"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 := appcontext.WithUser(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)
})
}