diff --git a/pkg/services/cloudmigration/api/api_test.go b/pkg/services/cloudmigration/api/api_test.go index 3a5fe5a1227..8126d71de0b 100644 --- a/pkg/services/cloudmigration/api/api_test.go +++ b/pkg/services/cloudmigration/api/api_test.go @@ -1,12 +1,454 @@ package api import ( + "io" + "net/http" + "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/cloudmigration/cloudmigrationimpl/fake" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web/webtest" + "github.com/stretchr/testify/require" ) -func Test_OnlyEnabledForGrafanaAdmi(t *testing.T) { - // TODO: implement - assert.True(t, true) +type TestCase struct { + desc string + requestHttpMethod string + requestUrl string + requestBody string + basicRole org.RoleType + // if the CloudMigrationService should return an error + serviceReturnError bool + expectedHttpResult int + expectedBody string +} + +func TestCloudMigrationAPI_GetToken(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"id":"mock_id","displayName":"mock_name","expiresAt":"","firstUsedAt":"","lastUsedAt":"","createdAt":""}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_CreateToken(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"token":"mock_token"}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/token", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_DeleteToken(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/token/1234", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusNoContent, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/token/1234", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/token/1234", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/token/***", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_GetMigration(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusNotFound, + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/****", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_GetMigrationList(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"migrations":[{"uid":"mock_uid_1","stack":"mock_stack_1","created":"2024-06-05T17:30:40Z","updated":"2024-06-05T17:30:40Z"},{"uid":"mock_uid_2","stack":"mock_stack_2","created":"2024-06-05T17:30:40Z","updated":"2024-06-05T17:30:40Z"}]}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_CreateMigration(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration", + requestBody: `{"auth_token":"asdf"}`, + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"uid":"fake_uid","stack":"fake_stack","created":"2024-06-05T17:30:40Z","updated":"2024-06-05T17:30:40Z"}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration", + requestBody: `{"authToken":"asdf"}`, + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + { + desc: "should return 400 if body is not a valid json", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration", + requestBody: "asdf", + basicRole: org.RoleAdmin, + serviceReturnError: false, + expectedHttpResult: http.StatusBadRequest, + expectedBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_RunMigration(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","error":"none"}]}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodPost, + requestUrl: "/api/cloudmigration/migration/***/run", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_GetMigrationRun(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/run/1234", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"uid":"fake_uid","items":[{"type":"type","refId":"make_refid","status":"ok","error":"none"}]}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/run/1234", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/run/1234", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/run/****", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_GetMigrationRunList(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"runs":[{"uid":"fake_run_uid_1"},{"uid":"fake_run_uid_2"}]}`, + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/run", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/****/run", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func TestCloudMigrationAPI_DeleteMigration(t *testing.T) { + tests := []TestCase{ + { + desc: "should return 200 if everything is ok", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: "", + }, + { + desc: "should return 403 if no used is not admin", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleEditor, + expectedHttpResult: http.StatusForbidden, + expectedBody: "", + }, + { + desc: "should return 500 if service returns an error", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/migration/1234", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusInternalServerError, + expectedBody: "", + }, + { + desc: "should return 400 if uid is invalid", + requestHttpMethod: http.MethodDelete, + requestUrl: "/api/cloudmigration/migration/****", + basicRole: org.RoleAdmin, + serviceReturnError: true, + expectedHttpResult: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, runSimpleApiTest(tt)) + } +} + +func runSimpleApiTest(tt TestCase) func(t *testing.T) { + return func(t *testing.T) { + // setup server + api := RegisterApi(routing.NewRouteRegister(), fake.FakeServiceImpl{ReturnError: tt.serviceReturnError}, tracing.InitializeTracerForTest()) + server := webtest.NewServer(t, api.routeRegister) + + var body io.Reader = nil + if tt.requestBody != "" { + body = strings.NewReader(tt.requestBody) + } + req := server.NewRequest(tt.requestHttpMethod, tt.requestUrl, body) + req.Header.Set("Content-Type", "application/json") + + // create test request + webtest.RequestWithSignedInUser(req, &user.SignedInUser{ + OrgID: 1, + OrgRole: tt.basicRole, + }) + res, err := server.Send(req) + defer func() { require.NoError(t, res.Body.Close()) }() + // validations + require.NoError(t, err) + require.Equal(t, tt.expectedHttpResult, res.StatusCode) + if tt.expectedBody != "" { + require.NotNil(t, t, res.Body) + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, tt.expectedBody, string(b)) + } + } } diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index 2ef57aeff9c..bb3d82af074 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -7,22 +7,21 @@ import ( ) type Service interface { - // Returns the cloud migration token if it exists. - GetToken(context.Context) (gcom.TokenView, error) - // Creates a cloud migration token. - CreateToken(context.Context) (CreateAccessTokenResponse, error) - // Sends a request to CMS to test the token. - ValidateToken(context.Context, CloudMigration) error + // GetToken Returns the cloud migration token if it exists. + GetToken(ctx context.Context) (gcom.TokenView, error) + // CreateToken Creates a cloud migration token. + CreateToken(ctx context.Context) (CreateAccessTokenResponse, error) + // ValidateToken Sends a request to CMS to test the token. + ValidateToken(ctx context.Context, mig CloudMigration) error DeleteToken(ctx context.Context, uid string) error - CreateMigration(context.Context, CloudMigrationRequest) (*CloudMigrationResponse, error) - GetMigration(ctx context.Context, uid string) (*CloudMigration, error) - DeleteMigration(ctx context.Context, uid string) (*CloudMigration, error) - UpdateMigration(ctx context.Context, uid string, request CloudMigrationRequest) (*CloudMigrationResponse, error) + CreateMigration(ctx context.Context, req CloudMigrationRequest) (*CloudMigrationResponse, error) + GetMigration(ctx context.Context, migUID string) (*CloudMigration, error) + DeleteMigration(ctx context.Context, migUID string) (*CloudMigration, error) + UpdateMigration(ctx context.Context, migUID string, request CloudMigrationRequest) (*CloudMigrationResponse, error) GetMigrationList(context.Context) (*CloudMigrationListResponse, error) - RunMigration(ctx context.Context, uid string) (*MigrateDataResponseDTO, error) - CreateMigrationRun(context.Context, CloudMigrationRun) (string, error) + RunMigration(ctx context.Context, migUID string) (*MigrateDataResponseDTO, error) GetMigrationStatus(ctx context.Context, runUID string) (*CloudMigrationRun, error) - GetMigrationRunList(context.Context, string) (*CloudMigrationRunList, error) + GetMigrationRunList(ctx context.Context, migUID string) (*CloudMigrationRunList, error) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index 0616f77d399..ec658947b6c 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -411,7 +411,7 @@ func (s *Service) RunMigration(ctx context.Context, uid string) (*cloudmigration } // save the result of the migration - runUID, err := s.CreateMigrationRun(ctx, cloudmigration.CloudMigrationRun{ + runUID, err := s.createMigrationRun(ctx, cloudmigration.CloudMigrationRun{ CloudMigrationUID: migration.UID, Result: respData, }) @@ -547,7 +547,7 @@ func (s *Service) getDashboards(ctx context.Context) ([]dashboards.Dashboard, er return result, nil } -func (s *Service) CreateMigrationRun(ctx context.Context, cmr cloudmigration.CloudMigrationRun) (string, error) { +func (s *Service) createMigrationRun(ctx context.Context, cmr cloudmigration.CloudMigrationRun) (string, error) { uid, err := s.store.CreateMigrationRun(ctx, cmr) if err != nil { s.log.Error("Failed to save migration run", "err", err) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 73140be8b9d..05ba69be725 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -4,8 +4,27 @@ import ( "context" "testing" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" + datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" + "github.com/grafana/grafana/pkg/services/folder/foldertest" + secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func Test_NoopServiceDoesNothing(t *testing.T) { @@ -13,3 +32,143 @@ func Test_NoopServiceDoesNothing(t *testing.T) { _, e := s.CreateToken(context.Background()) assert.ErrorIs(t, e, cloudmigration.ErrFeatureDisabledError) } + +func Test_CreateGetAndDeleteToken(t *testing.T) { + s := setUpServiceTest(t, false) + + createResp, err := s.CreateToken(context.Background()) + assert.NoError(t, err) + assert.NotEmpty(t, createResp.Token) + + token, err := s.GetToken(context.Background()) + assert.NoError(t, err) + assert.NotEmpty(t, token.Name) + + err = s.DeleteToken(context.Background(), token.ID) + assert.NoError(t, err) + + _, err = s.GetToken(context.Background()) + assert.ErrorIs(t, cloudmigration.ErrTokenNotFound, err) + + cm := cloudmigration.CloudMigration{} + err = s.ValidateToken(context.Background(), cm) + assert.NoError(t, err) +} + +func Test_CreateGetRunMigrationsAndRuns(t *testing.T) { + s := setUpServiceTest(t, true) + + createTokenResp, err := s.CreateToken(context.Background()) + assert.NoError(t, err) + assert.NotEmpty(t, createTokenResp.Token) + + cmd := cloudmigration.CloudMigrationRequest{ + AuthToken: createTokenResp.Token, + } + + createResp, err := s.CreateMigration(context.Background(), cmd) + require.NoError(t, err) + require.NotEmpty(t, createResp.UID) + require.NotEmpty(t, createResp.Stack) + + getMigResp, err := s.GetMigration(context.Background(), createResp.UID) + require.NoError(t, err) + require.NotNil(t, getMigResp) + require.Equal(t, createResp.UID, getMigResp.UID) + require.Equal(t, createResp.Stack, getMigResp.Stack) + + listResp, err := s.GetMigrationList(context.Background()) + require.NoError(t, err) + require.NotNil(t, listResp) + require.Equal(t, 1, len(listResp.Migrations)) + require.Equal(t, createResp.UID, listResp.Migrations[0].UID) + require.Equal(t, createResp.Stack, listResp.Migrations[0].Stack) + + runResp, err := s.RunMigration(ctxWithSignedInUser(), createResp.UID) + require.NoError(t, err) + require.NotNil(t, runResp) + resultItemsByType := make(map[string]int) + for _, item := range runResp.Items { + resultItemsByType[string(item.Type)] = resultItemsByType[string(item.Type)] + 1 + } + require.Equal(t, 1, resultItemsByType["DASHBOARD"]) + require.Equal(t, 2, resultItemsByType["DATASOURCE"]) + require.Equal(t, 2, len(resultItemsByType)) + + runStatusResp, err := s.GetMigrationStatus(context.Background(), runResp.RunUID) + require.NoError(t, err) + require.Equal(t, runResp.RunUID, runStatusResp.UID) + + listRunResp, err := s.GetMigrationRunList(context.Background(), createResp.UID) + require.NoError(t, err) + require.Equal(t, 1, len(listRunResp.Runs)) + require.Equal(t, runResp.RunUID, listRunResp.Runs[0].RunUID) + + delMigResp, err := s.DeleteMigration(context.Background(), createResp.UID) + require.NoError(t, err) + require.NotNil(t, createResp.UID, delMigResp.UID) +} + +func ctxWithSignedInUser() context.Context { + c := &contextmodel.ReqContext{ + SignedInUser: &user.SignedInUser{OrgID: 1}, + } + k := ctxkey.Key{} + ctx := context.WithValue(context.Background(), k, c) + return ctx +} + +func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Service { + sqlStore := db.InitTestDB(t) + secretsService := secretsfakes.NewFakeSecretsService() + rr := routing.NewRouteRegister() + spanRecorder := tracetest.NewSpanRecorder() + tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) + mockFolder := &foldertest.FakeService{ + ExpectedFolder: &folder.Folder{UID: "folderUID", Title: "Folder"}, + } + + cfg := setting.NewCfg() + section, err := cfg.Raw.NewSection("cloud_migration") + require.NoError(t, err) + _, err = section.NewKey("domain", "localhost:1234") + require.NoError(t, err) + // dont know if this is the best, but dont want to refactor at the moment + cfg.CloudMigration.IsDeveloperMode = true + + dashboardService := dashboards.NewFakeDashboardService(t) + if withDashboardMock { + dashboardService.On("GetAllDashboards", mock.Anything).Return( + []*dashboards.Dashboard{ + { + UID: "1", + Data: simplejson.New(), + }, + }, + nil, + ) + } + + dsService := &datafakes.FakeDataSourceService{ + DataSources: []*datasources.DataSource{ + {Name: "mmm", Type: "mysql"}, + {Name: "ZZZ", Type: "infinity"}, + }, + } + + s, err := ProvideService( + cfg, + featuremgmt.WithFeatures(featuremgmt.FlagOnPremToCloudMigrations), + sqlStore, + dsService, + secretsService, + rr, + prometheus.DefaultRegisterer, + tracer, + dashboardService, + mockFolder, + ) + require.NoError(t, err) + + return s +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go new file mode 100644 index 00000000000..8448a6e7074 --- /dev/null +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -0,0 +1,139 @@ +package fake + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/services/cloudmigration" + "github.com/grafana/grafana/pkg/services/gcom" +) + +var fixedDate = time.Date(2024, 6, 5, 17, 30, 40, 0, time.UTC) + +// FakeServiceImpl fake implementation of cloudmigration.Service for testing purposes +type FakeServiceImpl struct { + ReturnError bool +} + +var _ cloudmigration.Service = (*FakeServiceImpl)(nil) + +func (m FakeServiceImpl) GetToken(_ context.Context) (gcom.TokenView, error) { + if m.ReturnError { + return gcom.TokenView{}, fmt.Errorf("mock error") + } + return gcom.TokenView{ID: "mock_id", DisplayName: "mock_name"}, nil +} + +func (m FakeServiceImpl) CreateToken(_ context.Context) (cloudmigration.CreateAccessTokenResponse, error) { + if m.ReturnError { + return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("mock error") + } + return cloudmigration.CreateAccessTokenResponse{Token: "mock_token"}, nil +} + +func (m FakeServiceImpl) ValidateToken(ctx context.Context, migration cloudmigration.CloudMigration) error { + panic("implement me") +} + +func (m FakeServiceImpl) DeleteToken(_ context.Context, _ string) error { + if m.ReturnError { + return fmt.Errorf("mock error") + } + return nil +} + +func (m FakeServiceImpl) CreateMigration(_ context.Context, _ cloudmigration.CloudMigrationRequest) (*cloudmigration.CloudMigrationResponse, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + return &cloudmigration.CloudMigrationResponse{ + UID: "fake_uid", + Stack: "fake_stack", + Created: fixedDate, + Updated: fixedDate, + }, nil +} + +func (m FakeServiceImpl) GetMigration(_ context.Context, _ string) (*cloudmigration.CloudMigration, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + return &cloudmigration.CloudMigration{UID: "fake"}, nil +} + +func (m FakeServiceImpl) DeleteMigration(_ context.Context, _ string) (*cloudmigration.CloudMigration, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + return &cloudmigration.CloudMigration{UID: "fake"}, nil +} + +func (m FakeServiceImpl) UpdateMigration(ctx context.Context, uid string, request cloudmigration.CloudMigrationRequest) (*cloudmigration.CloudMigrationResponse, error) { + panic("implement me") +} + +func (m FakeServiceImpl) GetMigrationList(_ context.Context) (*cloudmigration.CloudMigrationListResponse, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + return &cloudmigration.CloudMigrationListResponse{ + Migrations: []cloudmigration.CloudMigrationResponse{ + {UID: "mock_uid_1", Stack: "mock_stack_1", Created: fixedDate, Updated: fixedDate}, + {UID: "mock_uid_2", Stack: "mock_stack_2", Created: fixedDate, Updated: fixedDate}, + }, + }, nil +} + +func (m FakeServiceImpl) RunMigration(_ context.Context, _ string) (*cloudmigration.MigrateDataResponseDTO, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + r := fakeMigrateDataResponseDTO() + return &r, nil +} + +func fakeMigrateDataResponseDTO() cloudmigration.MigrateDataResponseDTO { + return cloudmigration.MigrateDataResponseDTO{ + RunUID: "fake_uid", + Items: []cloudmigration.MigrateDataResponseItemDTO{ + {Type: "type", RefID: "make_refid", Status: "ok", Error: "none"}, + }, + } +} + +func (m FakeServiceImpl) CreateMigrationRun(ctx context.Context, run cloudmigration.CloudMigrationRun) (string, error) { + panic("implement me") +} + +func (m FakeServiceImpl) GetMigrationStatus(_ context.Context, _ string) (*cloudmigration.CloudMigrationRun, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + result, err := json.Marshal(fakeMigrateDataResponseDTO()) + if err != nil { + return nil, err + } + return &cloudmigration.CloudMigrationRun{ + ID: 0, + UID: "fake_uid", + CloudMigrationUID: "fake_mig_uid", + Result: result, + Created: fixedDate, + Updated: fixedDate, + Finished: fixedDate, + }, nil +} + +func (m FakeServiceImpl) GetMigrationRunList(_ context.Context, _ string) (*cloudmigration.CloudMigrationRunList, error) { + if m.ReturnError { + return nil, fmt.Errorf("mock error") + } + return &cloudmigration.CloudMigrationRunList{ + Runs: []cloudmigration.MigrateDataResponseListDTO{ + {RunUID: "fake_run_uid_1"}, + {RunUID: "fake_run_uid_2"}, + }, + }, nil +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 7c3c6c945d0..f7fb0234eb8 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -1,48 +1,208 @@ package cloudmigrationimpl import ( + "context" + "encoding/base64" + "strconv" "testing" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/cloudmigration" + fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { testsuite.Run(m) } -// TODO rewrite this to include encoding and decryption -// func TestGetAllCloudMigrations(t *testing.T) { -// testDB := db.InitTestDB(t) -// s := &sqlStore{db: testDB} -// ctx := context.Background() +func Test_GetAllCloudMigrations(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() -// t.Run("get all cloud_migrations", func(t *testing.T) { -// // replace this with proper method when created -// _, err := testDB.GetSqlxSession().Exec(ctx, ` -// INSERT INTO cloud_migration (id, auth_token, stack, stack_id, region_slug, cluster_slug, created, updated) -// VALUES (1, '12345', '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), -// (2, '6789', '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), -// (3, '777', '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); -// `) -// require.NoError(t, err) + t.Run("get all cloud_migrations", func(t *testing.T) { + value, err := s.GetAllCloudMigrations(ctx) + require.NoError(t, err) + require.Equal(t, 3, len(value)) + for _, m := range value { + switch m.ID { + case 1: + require.Equal(t, "11111", m.Stack) + require.Equal(t, "12345", m.AuthToken) + case 2: + require.Equal(t, "22222", m.Stack) + require.Equal(t, "6789", m.AuthToken) + case 3: + require.Equal(t, "33333", m.Stack) + require.Equal(t, "777", m.AuthToken) + default: + require.Fail(t, "ID value not expected: "+strconv.FormatInt(m.ID, 10)) + } + } + }) +} -// value, err := s.GetAllCloudMigrations(ctx) -// require.NoError(t, err) -// require.Equal(t, 3, len(value)) -// for _, m := range value { -// switch m.ID { -// case 1: -// require.Equal(t, "11111", m.Stack) -// require.Equal(t, "12345", m.AuthToken) -// case 2: -// require.Equal(t, "22222", m.Stack) -// require.Equal(t, "6789", m.AuthToken) -// case 3: -// require.Equal(t, "33333", m.Stack) -// require.Equal(t, "777", m.AuthToken) -// default: -// require.Fail(t, "ID value not expected: "+strconv.FormatInt(m.ID, 10)) -// } -// } -// }) -// } +func Test_CreateMigration(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + + t.Run("creates migrations and reads it from the db", func(t *testing.T) { + cm := cloudmigration.CloudMigration{ + AuthToken: encodeToken("token"), + Stack: "fake_stack", + StackID: 1234, + RegionSlug: "fake_slug", + ClusterSlug: "fake_cluster_slug", + } + mig, err := s.CreateMigration(ctx, cm) + require.NoError(t, err) + require.NotEmpty(t, mig.ID) + require.NotEmpty(t, mig.UID) + + getRes, err := s.GetMigrationByUID(ctx, mig.UID) + require.NoError(t, err) + require.Equal(t, mig.ID, getRes.ID) + require.Equal(t, mig.UID, getRes.UID) + require.Equal(t, cm.AuthToken, getRes.AuthToken) + require.Equal(t, cm.Stack, getRes.Stack) + require.Equal(t, cm.StackID, getRes.StackID) + require.Equal(t, cm.RegionSlug, getRes.RegionSlug) + require.Equal(t, cm.ClusterSlug, getRes.ClusterSlug) + }) +} + +func Test_GetMigrationByUID(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + t.Run("find migration by uid", func(t *testing.T) { + uid := "qwerty" + mig, err := s.GetMigrationByUID(ctx, uid) + require.NoError(t, err) + require.Equal(t, uid, mig.UID) + }) + + t.Run("returns error if migration is not found by uid", func(t *testing.T) { + _, err := s.GetMigrationByUID(ctx, "fake_uid_1234") + require.ErrorIs(t, cloudmigration.ErrMigrationNotFound, err) + }) +} + +func Test_DeleteMigration(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + + t.Run("deletes a migration from the db", func(t *testing.T) { + uid := "qwerty" + delResp, err := s.DeleteMigration(ctx, uid) + require.NoError(t, err) + require.Equal(t, uid, delResp.UID) + + // now we try to find it, should return an error + _, err = s.GetMigrationByUID(ctx, uid) + require.ErrorIs(t, cloudmigration.ErrMigrationNotFound, err) + }) +} + +func Test_CreateMigrationRun(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + + t.Run("creates a migration run and retrieves it from db", func(t *testing.T) { + result := []byte("OK") + cmr := cloudmigration.CloudMigrationRun{ + CloudMigrationUID: "asdfg", + Result: result, + } + + createResp, err := s.CreateMigrationRun(ctx, cmr) + require.NoError(t, err) + require.NotEmpty(t, createResp) + + getMRResp, err := s.GetMigrationStatus(ctx, createResp) + require.NoError(t, err) + require.Equal(t, result, getMRResp.Result) + }) +} + +func Test_GetMigrationStatus(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + + t.Run("gets a migration status by uid", func(t *testing.T) { + getMRResp, err := s.GetMigrationStatus(ctx, "poiuy") + require.NoError(t, err) + require.Equal(t, "poiuy", getMRResp.UID) + }) + + t.Run("returns error if migration run was not found", func(t *testing.T) { + getMRResp, err := s.GetMigrationStatus(ctx, "fake_uid") + require.ErrorIs(t, cloudmigration.ErrMigrationRunNotFound, err) + require.Equal(t, int64(0), getMRResp.ID) + require.Equal(t, "", getMRResp.UID) + }) +} + +func Test_GetMigrationStatusList(t *testing.T) { + _, s := setUpTest(t) + ctx := context.Background() + + t.Run("gets migration status list from db", func(t *testing.T) { + list, err := s.GetMigrationStatusList(ctx, "qwerty") + require.NoError(t, err) + require.Equal(t, 2, len(list)) + // TODO validate that this is ok + }) + + t.Run("returns no error if migration was not found, just empty list", func(t *testing.T) { + list, err := s.GetMigrationStatusList(ctx, "fake_migration") + require.NoError(t, err) + require.Equal(t, 0, len(list)) + }) +} + +func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { + testDB := db.InitTestDB(t) + s := &sqlStore{ + db: testDB, + secretsService: fakeSecrets.FakeSecretsService{}, + } + ctx := context.Background() + + // insert cloud migration test data + _, err := testDB.GetSqlxSession().Exec(ctx, ` + INSERT INTO + cloud_migration (id, uid, auth_token, stack, stack_id, region_slug, cluster_slug, created, updated) + VALUES + (1,'qwerty', ?, '11111', 11111, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (2,'asdfgh', ?, '22222', 22222, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'), + (3,'zxcvbn', ?, '33333', 33333, 'test', 'test', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000'); + `, + encodeToken("12345"), + encodeToken("6789"), + encodeToken("777"), + ) + require.NoError(t, err) + + // insert cloud migration run test data + _, err = testDB.GetSqlxSession().Exec(ctx, ` + INSERT INTO + cloud_migration_run (cloud_migration_uid, uid, result, created, updated, finished) + VALUES + ('qwerty', 'poiuy', ?, '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000'), + ('qwerty', 'lkjhg', ?, '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000'), + ('zxcvbn', 'mnbvvc', ?, '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000'); + `, + []byte("ERROR"), + []byte("OK"), + []byte("OK"), + ) + require.NoError(t, err) + + return testDB, s +} + +func encodeToken(t string) string { + return base64.StdEncoding.EncodeToString([]byte(t)) +}