* fix: graceful handling by default * fix: make fallback the default behavior
663 lines
25 KiB
Go
663 lines
25 KiB
Go
package legacy
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
|
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
|
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/storage/legacysql"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
|
|
)
|
|
|
|
func TestScanRow(t *testing.T) {
|
|
mockDB, mock, err := sqlmock.New()
|
|
require.NoError(t, err)
|
|
defer mockDB.Close() // nolint:errcheck
|
|
|
|
pathToFile := "path/to/file"
|
|
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
|
|
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
|
|
store := &dashboardSqlAccess{
|
|
namespacer: func(_ int64) string { return "default" },
|
|
provisioning: provisioner,
|
|
log: log.New("test"),
|
|
}
|
|
|
|
columns := []string{"orgId", "dashboard_id", "name", "title", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"}
|
|
id := int64(100)
|
|
uid := "someuid"
|
|
title := "Test Dashboard"
|
|
folderUID := "folder123"
|
|
timestamp := time.Now()
|
|
k8sTimestamp := metav1.Time{Time: timestamp}
|
|
version := int64(2)
|
|
message := "updated message"
|
|
createdUser := "creator"
|
|
updatedUser := "updator"
|
|
|
|
t.Run("Should scan a valid row correctly", func(t *testing.T) {
|
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
|
resultRows, err := mockDB.Query("SELECT *")
|
|
require.NoError(t, err)
|
|
defer resultRows.Close() // nolint:errcheck
|
|
resultRows.Next()
|
|
|
|
row, err := store.scanRow(resultRows, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, row)
|
|
require.Equal(t, uid, row.Dash.Name)
|
|
require.Equal(t, version, row.RV) // rv should be the dashboard version
|
|
require.Equal(t, common.Unstructured{
|
|
Object: map[string]interface{}{"key": "value"},
|
|
}, row.Dash.Spec)
|
|
require.Equal(t, "default", row.Dash.Namespace)
|
|
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
|
|
|
|
meta, err := utils.MetaAccessor(row.Dash)
|
|
require.NoError(t, err)
|
|
require.Equal(t, id, meta.GetDeprecatedInternalID()) // nolint:staticcheck
|
|
require.Equal(t, version, meta.GetGeneration()) // generation should be dash version
|
|
require.Equal(t, k8sTimestamp, meta.GetCreationTimestamp())
|
|
require.Equal(t, "user:"+createdUser, meta.GetCreatedBy()) // should be prefixed by user:
|
|
require.Equal(t, "user:"+updatedUser, meta.GetUpdatedBy()) // should be prefixed by user:
|
|
require.Equal(t, message, meta.GetMessage())
|
|
require.Equal(t, folderUID, meta.GetFolder())
|
|
require.Equal(t, "dashboard.grafana.app/vXyz", row.Dash.APIVersion)
|
|
})
|
|
|
|
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
|
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
|
resultRows, err := mockDB.Query("SELECT *")
|
|
require.NoError(t, err)
|
|
defer resultRows.Close() // nolint:errcheck
|
|
resultRows.Next()
|
|
|
|
row, err := store.scanRow(resultRows, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, row)
|
|
|
|
meta, err := utils.MetaAccessor(row.Dash)
|
|
require.NoError(t, err)
|
|
m, ok := meta.GetManagerProperties()
|
|
require.True(t, ok)
|
|
|
|
s, ok := meta.GetSourceProperties()
|
|
require.True(t, ok)
|
|
|
|
require.Equal(t, utils.ManagerKindClassicFP, m.Kind) // nolint:staticcheck
|
|
require.Equal(t, "provisioner", m.Identity)
|
|
require.Equal(t, pathToFile, s.Path)
|
|
require.Equal(t, "hashing", s.Checksum)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(100000), s.TimestampMillis)
|
|
})
|
|
|
|
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
|
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
|
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
|
resultRows, err := mockDB.Query("SELECT *")
|
|
require.NoError(t, err)
|
|
defer resultRows.Close() // nolint:errcheck
|
|
resultRows.Next()
|
|
|
|
row, err := store.scanRow(resultRows, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, row)
|
|
|
|
meta, err := utils.MetaAccessor(row.Dash)
|
|
require.NoError(t, err)
|
|
manager, ok := meta.GetManagerProperties()
|
|
require.True(t, ok)
|
|
|
|
require.Equal(t, utils.ManagerKindPlugin, manager.Kind)
|
|
require.Equal(t, "slo", manager.Identity) // the ID of the plugin
|
|
require.Equal(t, "", meta.GetAnnotations()[utils.AnnoKeySourceChecksum]) // hash is not used on plugins
|
|
})
|
|
|
|
t.Run("Migration scenario should use COALESCE logic with AllowFallback=true", func(t *testing.T) {
|
|
// This specifically tests the migration use case where GetHistory=true but AllowFallback=true
|
|
// allows the query to use COALESCE logic to fall back to dashboard table data when
|
|
// dashboard_version entries are missing.
|
|
|
|
migrationTimestamp := timestamp.Add(2 * time.Hour) // Migration scenario timestamp
|
|
migrationVersion := int64(5)
|
|
migrationUpdatedUser := "migration_updater"
|
|
migrationMessage := "" // Empty message for migration (COALESCE behavior)
|
|
migrationData := []byte(`{"migration": "data", "title": "Migrated Dashboard"}`)
|
|
migrationAPIVersion := "v0alpha1"
|
|
|
|
// In migration scenario, COALESCE functions return dashboard table values
|
|
// when dashboard_version values are NULL, ensuring all dashboards are migrated
|
|
rows := sqlmock.NewRows(columns).AddRow(
|
|
1, id, uid, title, folderUID, nil, "", // basic dashboard fields
|
|
"", "", "", 0, // origin fields
|
|
timestamp, createdUser, 0, // created fields
|
|
// These represent COALESCED values from dashboard table (not version table)
|
|
migrationTimestamp, migrationUpdatedUser, 0, migrationVersion, migrationMessage, migrationData, migrationAPIVersion,
|
|
)
|
|
|
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
|
resultRows, err := mockDB.Query("SELECT *")
|
|
require.NoError(t, err)
|
|
defer resultRows.Close() // nolint:errcheck
|
|
resultRows.Next()
|
|
|
|
// Test with history=true (migration scenario) - should work with COALESCED values
|
|
row, err := store.scanRow(resultRows, true)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, row)
|
|
|
|
// Verify migration scenario works correctly with fallback data
|
|
require.Equal(t, uid, row.Dash.Name)
|
|
require.Equal(t, "Migrated Dashboard", row.Dash.Spec.Object["title"])
|
|
require.Equal(t, migrationVersion, row.RV) // Should use COALESCEd dashboard table version
|
|
require.Equal(t, common.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"migration": "data",
|
|
"title": "Migrated Dashboard",
|
|
},
|
|
}, row.Dash.Spec) // Should use COALESCEd dashboard table data
|
|
require.Equal(t, "default", row.Dash.Namespace)
|
|
|
|
// Token should use the version in history mode (migration scenario)
|
|
require.Equal(t, &continueToken{orgId: int64(1), id: migrationVersion}, row.token)
|
|
|
|
meta, err := utils.MetaAccessor(row.Dash)
|
|
require.NoError(t, err)
|
|
require.Equal(t, id, meta.GetDeprecatedInternalID()) // nolint:staticcheck
|
|
require.Equal(t, migrationVersion, meta.GetGeneration()) // Should use COALESCEd version
|
|
require.Equal(t, k8sTimestamp, meta.GetCreationTimestamp()) // Created timestamp unchanged
|
|
require.Equal(t, "user:"+createdUser, meta.GetCreatedBy()) // Original creator preserved
|
|
require.Equal(t, "user:"+migrationUpdatedUser, meta.GetUpdatedBy()) // COALESCEd updater
|
|
require.Equal(t, migrationMessage, meta.GetMessage()) // Empty message from COALESCE
|
|
require.Equal(t, folderUID, meta.GetFolder())
|
|
require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion)
|
|
})
|
|
|
|
t.Run("should follow dashboard template when failing to unmarshal dashboard", func(t *testing.T) {
|
|
// row with bad data
|
|
badData := []byte(`{"rows":[{"panels":[{"targets":[{"refId":"A","target":"aliasSub(alias, '^(.{27}).+', '\1...')"}]}]}]}`)
|
|
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, badData, "vXyz")
|
|
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
|
|
resultRows, err := mockDB.Query("SELECT *")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
_ = resultRows.Close()
|
|
}()
|
|
resultRows.Next()
|
|
|
|
store = &dashboardSqlAccess{
|
|
namespacer: func(_ int64) string { return "default" },
|
|
provisioning: provisioner,
|
|
log: log.New("test"),
|
|
}
|
|
|
|
row, err := store.scanRow(resultRows, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, row)
|
|
require.Equal(t, uid, row.Dash.Name)
|
|
require.Equal(t, version, row.RV)
|
|
require.Equal(t, "default", row.Dash.Namespace)
|
|
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
|
|
|
|
// instead of failing, create dummy dashboard with broken json inlined in text panel
|
|
require.Equal(t, title, row.Dash.Spec.Object["title"])
|
|
panels, exists := row.Dash.Spec.Object["panels"]
|
|
require.True(t, exists, "panels property should exist")
|
|
|
|
panelsSlice, ok := panels.([]interface{})
|
|
require.True(t, ok, "panels should be a slice")
|
|
require.Len(t, panelsSlice, 1, "panels should have exactly one element")
|
|
|
|
panel, ok := panelsSlice[0].(map[string]interface{})
|
|
require.True(t, ok, "panel should be a map")
|
|
|
|
options, exists := panel["options"]
|
|
require.True(t, exists, "panel should have options property")
|
|
|
|
optionsMap, ok := options.(map[string]interface{})
|
|
require.True(t, ok, "options should be a map")
|
|
|
|
content, exists := optionsMap["content"]
|
|
require.True(t, exists, "options should have content property")
|
|
|
|
contentStr, ok := content.(string)
|
|
require.True(t, ok, "content should be a string")
|
|
require.Equal(t, string(badData), contentStr, "content should match bad json data")
|
|
})
|
|
}
|
|
|
|
func TestBuildSaveDashboardCommand(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
schemaVersion int
|
|
expectedAPI string
|
|
}{
|
|
{
|
|
name: "with schema version 36 should save as v0",
|
|
schemaVersion: 36,
|
|
expectedAPI: dashboardV0.VERSION,
|
|
},
|
|
// {
|
|
// name: "with schema version 41 should save as v1",
|
|
// schemaVersion: 41,
|
|
// expectedAPI: dashboardV1.VERSION,
|
|
// },
|
|
// {
|
|
// name: "with empty schema version should save as v0",
|
|
// schemaVersion: 0,
|
|
// expectedAPI: dashboardV0.VERSION,
|
|
// },
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mockStore := &dashboards.FakeDashboardStore{}
|
|
access := &dashboardSqlAccess{
|
|
dashStore: mockStore,
|
|
log: log.New("test"),
|
|
}
|
|
|
|
dashSpec := map[string]interface{}{
|
|
"title": "Test Dashboard",
|
|
"id": 123,
|
|
}
|
|
|
|
if tc.schemaVersion > 0 {
|
|
dashSpec["schemaVersion"] = tc.schemaVersion
|
|
}
|
|
|
|
dash := &dashboardV1.Dashboard{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: dashboardV1.APIVERSION,
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dash",
|
|
},
|
|
Spec: common.Unstructured{
|
|
Object: dashSpec,
|
|
},
|
|
}
|
|
|
|
// fail if no user in context
|
|
_, _, err := access.buildSaveDashboardCommand(context.Background(), 1, dash)
|
|
require.Error(t, err)
|
|
|
|
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{
|
|
OrgID: 1,
|
|
OrgRole: "Admin",
|
|
})
|
|
// create new dashboard
|
|
mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return(nil, nil).Once()
|
|
cmd, created, err := access.buildSaveDashboardCommand(ctx, 1, dash)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, created)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString())
|
|
_, exists := cmd.Dashboard.CheckGet("id")
|
|
require.False(t, exists) // id should be removed
|
|
require.Equal(t, cmd.OrgID, int64(1))
|
|
require.True(t, cmd.Overwrite)
|
|
require.Equal(t, tc.expectedAPI, cmd.APIVersion) // verify expected API version
|
|
|
|
// now update existing dashboard
|
|
mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return(
|
|
&dashboards.Dashboard{
|
|
ID: 1234,
|
|
Version: 2,
|
|
APIVersion: dashboardV1.VERSION,
|
|
}, nil).Once()
|
|
cmd, created, err = access.buildSaveDashboardCommand(ctx, 1, dash)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, created)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString())
|
|
require.Equal(t, cmd.Dashboard.Get("id").MustInt64(), int64(1234)) // should set to existing ID
|
|
require.Equal(t, cmd.Dashboard.Get("version").MustFloat64(), float64(2)) // version must be set - otherwise seen as a new dashboard in NewDashboardFromJson
|
|
require.Equal(t, tc.expectedAPI, cmd.APIVersion) // verify expected API version
|
|
require.Equal(t, cmd.OrgID, int64(1))
|
|
require.True(t, cmd.Overwrite)
|
|
})
|
|
}
|
|
|
|
t.Run("service account should have userID set", func(t *testing.T) {
|
|
mockStore := &dashboards.FakeDashboardStore{}
|
|
access := &dashboardSqlAccess{
|
|
dashStore: mockStore,
|
|
log: log.New("test"),
|
|
}
|
|
|
|
dash := &dashboardV1.Dashboard{
|
|
TypeMeta: metav1.TypeMeta{
|
|
APIVersion: dashboardV1.APIVERSION,
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dash",
|
|
},
|
|
Spec: common.Unstructured{
|
|
Object: map[string]interface{}{
|
|
"title": "Test Dashboard",
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{
|
|
UserID: 123,
|
|
OrgID: 1,
|
|
OrgRole: "Editor",
|
|
IsServiceAccount: true,
|
|
})
|
|
|
|
mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return(nil, nil).Once()
|
|
cmd, created, err := access.buildSaveDashboardCommand(ctx, 1, dash)
|
|
require.NoError(t, err)
|
|
require.True(t, created)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, int64(123), cmd.UserID, "service account user ID should be set correctly")
|
|
})
|
|
}
|
|
|
|
func TestParseLibraryPanelRow(t *testing.T) {
|
|
basePanel := panel{
|
|
ID: 1,
|
|
UID: "panel-uid",
|
|
FolderUID: sql.NullString{String: "folder-uid", Valid: true},
|
|
Created: time.Now(),
|
|
CreatedBy: sql.NullString{String: "creator", Valid: true},
|
|
Updated: time.Now(),
|
|
UpdatedBy: sql.NullString{String: "updator", Valid: true},
|
|
Version: 2,
|
|
Type: "graph",
|
|
Description: "desc from db",
|
|
}
|
|
|
|
t.Run("type mismatch triggers warning", func(t *testing.T) {
|
|
p := basePanel
|
|
p.Name = "Test Panel"
|
|
p.Type = "graph"
|
|
model := map[string]interface{}{
|
|
"title": "Test Panel",
|
|
"type": "table",
|
|
"description": "desc from db",
|
|
}
|
|
modelBytes, err := json.Marshal(model)
|
|
require.NoError(t, err)
|
|
p.Model = modelBytes
|
|
|
|
item, err := parseLibraryPanelRow(p)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "table", item.Spec.Type)
|
|
require.NotEmpty(t, item.Status.Warnings)
|
|
require.Contains(t, item.Status.Warnings[0], "type mismatch")
|
|
})
|
|
|
|
t.Run("metadata is set correctly", func(t *testing.T) {
|
|
p := basePanel
|
|
p.Name = "Test Panel"
|
|
// Ensure there's a time difference greater than 1 second to trigger updated metadata
|
|
p.Updated = p.Created.Add(2 * time.Second)
|
|
model := map[string]interface{}{
|
|
"title": "Test Panel",
|
|
"type": "graph",
|
|
"description": "desc from db",
|
|
}
|
|
modelBytes, err := json.Marshal(model)
|
|
require.NoError(t, err)
|
|
p.Model = modelBytes
|
|
|
|
item, err := parseLibraryPanelRow(p)
|
|
require.NoError(t, err)
|
|
|
|
meta, err := utils.MetaAccessor(&item)
|
|
require.NoError(t, err)
|
|
require.Equal(t, p.ID, meta.GetDeprecatedInternalID()) // nolint:staticcheck
|
|
require.Equal(t, p.Version, meta.GetGeneration())
|
|
require.Equal(t, p.FolderUID.String, meta.GetFolder())
|
|
require.Equal(t, "user:creator", meta.GetCreatedBy())
|
|
require.Equal(t, "user:updator", meta.GetUpdatedBy())
|
|
})
|
|
|
|
t.Run("panel title in dashboard vs library panel title is set correctly", func(t *testing.T) {
|
|
p := basePanel
|
|
p.Name = "Database Name"
|
|
model := map[string]interface{}{
|
|
"title": "Model Title",
|
|
"type": "graph",
|
|
"description": "desc from db",
|
|
}
|
|
modelBytes, err := json.Marshal(model)
|
|
require.NoError(t, err)
|
|
p.Model = modelBytes
|
|
|
|
item, err := parseLibraryPanelRow(p)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "Model Title", item.Spec.PanelTitle)
|
|
require.Equal(t, "Database Name", item.Spec.Title)
|
|
})
|
|
|
|
t.Run("handles NULL created_by and updated_by fields", func(t *testing.T) {
|
|
p := basePanel
|
|
p.Name = "Test Panel"
|
|
// Set CreatedBy and UpdatedBy to NULL (Invalid)
|
|
p.CreatedBy = sql.NullString{String: "", Valid: false}
|
|
p.UpdatedBy = sql.NullString{String: "", Valid: false}
|
|
model := map[string]interface{}{
|
|
"title": "Test Panel",
|
|
"type": "graph",
|
|
"description": "desc from db",
|
|
}
|
|
modelBytes, err := json.Marshal(model)
|
|
require.NoError(t, err)
|
|
p.Model = modelBytes
|
|
|
|
item, err := parseLibraryPanelRow(p)
|
|
require.NoError(t, err)
|
|
|
|
meta, err := utils.MetaAccessor(&item)
|
|
require.NoError(t, err)
|
|
|
|
// When CreatedBy is NULL, it should be set to empty string
|
|
require.Equal(t, "", meta.GetCreatedBy())
|
|
|
|
// When UpdatedBy is NULL and not valid, updated metadata should not be set
|
|
require.Equal(t, "", meta.GetUpdatedBy())
|
|
// The updated timestamp should not be set when UpdatedBy is invalid
|
|
updatedTimestamp, err := meta.GetUpdatedTimestamp()
|
|
require.NoError(t, err)
|
|
require.Nil(t, updatedTimestamp)
|
|
})
|
|
|
|
t.Run("handles valid created_by but NULL updated_by", func(t *testing.T) {
|
|
p := basePanel
|
|
p.Name = "Test Panel"
|
|
p.CreatedBy = sql.NullString{String: "creator", Valid: true}
|
|
p.UpdatedBy = sql.NullString{String: "", Valid: false}
|
|
// Make sure there's a time difference to trigger the update logic
|
|
p.Updated = p.Created.Add(10 * time.Second)
|
|
model := map[string]interface{}{
|
|
"title": "Test Panel",
|
|
"type": "graph",
|
|
"description": "desc from db",
|
|
}
|
|
modelBytes, err := json.Marshal(model)
|
|
require.NoError(t, err)
|
|
p.Model = modelBytes
|
|
|
|
item, err := parseLibraryPanelRow(p)
|
|
require.NoError(t, err)
|
|
|
|
meta, err := utils.MetaAccessor(&item)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "user:creator", meta.GetCreatedBy())
|
|
// Even with time difference, if UpdatedBy is not valid, updated metadata should not be set
|
|
require.Equal(t, "", meta.GetUpdatedBy())
|
|
updatedTimestamp, err := meta.GetUpdatedTimestamp()
|
|
require.NoError(t, err)
|
|
require.Nil(t, updatedTimestamp)
|
|
})
|
|
}
|
|
|
|
func TestDashboardMigrationQuery(t *testing.T) {
|
|
// Test that migration queries use AllowFallback flag correctly
|
|
nodb := &legacysql.LegacyDatabaseHelper{
|
|
Table: func(n string) string {
|
|
return "grafana." + n
|
|
},
|
|
}
|
|
|
|
t.Run("Migration query should enable AllowFallback flag", func(t *testing.T) {
|
|
// Create a migration query as would be used in actual migration
|
|
migrationQuery := &DashboardQuery{
|
|
OrgID: 1,
|
|
GetHistory: true, // Migration includes history
|
|
AllowFallback: true, // This is the key flag for migration
|
|
Order: "ASC", // Migration uses ascending order
|
|
}
|
|
|
|
// Verify UseHistoryTable returns true (requirement for COALESCE logic)
|
|
require.True(t, migrationQuery.UseHistoryTable(), "Migration query should use history table")
|
|
|
|
// Verify the flag is set correctly
|
|
require.True(t, migrationQuery.AllowFallback, "Migration query should allow fallback")
|
|
require.True(t, migrationQuery.GetHistory, "Migration query should get history")
|
|
require.Equal(t, "ASC", migrationQuery.Order, "Migration should use ascending order")
|
|
})
|
|
|
|
t.Run("Regular history query should not use AllowFallback", func(t *testing.T) {
|
|
// Regular history query without migration
|
|
historyQuery := &DashboardQuery{
|
|
OrgID: 1,
|
|
GetHistory: true,
|
|
Order: "DESC",
|
|
}
|
|
|
|
require.True(t, historyQuery.UseHistoryTable(), "History query should use history table")
|
|
require.False(t, historyQuery.AllowFallback, "Regular history query should not allow fallback")
|
|
require.True(t, historyQuery.GetHistory, "History query should get history")
|
|
})
|
|
|
|
t.Run("Migration query template produces COALESCE SQL", func(t *testing.T) {
|
|
// Test that the SQL template produces COALESCE logic for migration queries
|
|
migrationQuery := &DashboardQuery{
|
|
OrgID: 1,
|
|
GetHistory: true,
|
|
AllowFallback: true,
|
|
Order: "ASC",
|
|
}
|
|
|
|
req := newQueryReq(nodb, migrationQuery)
|
|
req.SQLTemplate = mocks.NewTestingSQLTemplate()
|
|
|
|
// Execute the template to get the generated SQL
|
|
rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req)
|
|
require.NoError(t, err)
|
|
|
|
sql := rawQuery
|
|
|
|
// Verify that COALESCE functions are present in the generated SQL
|
|
// These should be used when GetHistory=true AND AllowFallback=true
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)",
|
|
"Migration SQL should contain COALESCE for updated timestamp")
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version)",
|
|
"Migration SQL should contain COALESCE for version")
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.data, dashboard.data)",
|
|
"Migration SQL should contain COALESCE for data")
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.api_version, dashboard.api_version)",
|
|
"Migration SQL should contain COALESCE for api_version")
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.message, '')",
|
|
"Migration SQL should contain COALESCE for message with empty string fallback")
|
|
|
|
// Verify ORDER BY uses COALESCE as well
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated) ASC",
|
|
"Migration SQL should ORDER BY COALESCED created timestamp")
|
|
require.Contains(t, sql, "COALESCE(dashboard_version.version, dashboard.version) ASC",
|
|
"Migration SQL should ORDER BY COALESCED version")
|
|
|
|
// Verify it doesn't have the strict history table filter that would exclude NULL version entries
|
|
require.NotContains(t, sql, "dashboard_version.id IS NOT NULL",
|
|
"Migration SQL should not exclude dashboards without version entries")
|
|
})
|
|
|
|
t.Run("Regular history query produces strict SQL", func(t *testing.T) {
|
|
// Test that regular history queries still use strict dashboard_version fields
|
|
historyQuery := &DashboardQuery{
|
|
OrgID: 1,
|
|
GetHistory: true,
|
|
Order: "DESC",
|
|
}
|
|
|
|
req := newQueryReq(nodb, historyQuery)
|
|
req.SQLTemplate = mocks.NewTestingSQLTemplate()
|
|
|
|
rawQuery, err := sqltemplate.Execute(sqlQueryDashboards, &req)
|
|
require.NoError(t, err)
|
|
|
|
sql := rawQuery
|
|
|
|
// Verify that direct dashboard_version fields are used (no COALESCE)
|
|
require.Contains(t, sql, "dashboard_version.created as updated",
|
|
"Regular history SQL should use direct dashboard_version.created")
|
|
require.Contains(t, sql, "dashboard_version.version",
|
|
"Regular history SQL should use direct dashboard_version.version")
|
|
require.Contains(t, sql, "dashboard_version.data",
|
|
"Regular history SQL should use direct dashboard_version.data")
|
|
|
|
// NOTE: We intentionally do NOT add dashboard_version.id IS NOT NULL filter
|
|
// to allow for cases where dashboard_version entries might be missing
|
|
|
|
// Should not contain COALESCE functions
|
|
require.NotContains(t, sql, "COALESCE(dashboard_version.created, dashboard.updated)",
|
|
"Regular history SQL should not contain COALESCE for updated")
|
|
})
|
|
}
|
|
|
|
func TestMigrateDashboardsConfiguration(t *testing.T) {
|
|
// Test the actual migration function configuration
|
|
|
|
t.Run("Migration options should configure query correctly", func(t *testing.T) {
|
|
// Test the migration configuration as used in real migration
|
|
opts := MigrateOptions{
|
|
WithHistory: true, // Migration includes history
|
|
}
|
|
|
|
// This simulates what happens in migrateDashboards function
|
|
expectedQuery := &DashboardQuery{
|
|
OrgID: 1,
|
|
Limit: 100000000,
|
|
GetHistory: opts.WithHistory, // Should be true
|
|
AllowFallback: true, // Should be true for migration
|
|
Order: "ASC", // Should be ASC for migration
|
|
}
|
|
|
|
// Verify the configuration matches what migration sets up
|
|
require.True(t, expectedQuery.GetHistory, "Migration should enable GetHistory")
|
|
require.True(t, expectedQuery.AllowFallback, "Migration should enable AllowFallback")
|
|
require.Equal(t, "ASC", expectedQuery.Order, "Migration should use ascending order")
|
|
require.Equal(t, 100000000, expectedQuery.Limit, "Migration should use large limit")
|
|
|
|
// Verify UseHistoryTable logic
|
|
require.True(t, expectedQuery.UseHistoryTable(), "Migration query should use history table")
|
|
})
|
|
}
|