github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/openapi_controller_test.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  //
    14  // MVC for dm-master's openapi server
    15  // Model(data in etcd): source of truth
    16  // View(openapi_view): do some inner work such as validate, filter, prepare parameters/response and call controller to update model.
    17  // Controller(openapi_controller): call model func to update data.
    18  
    19  package master
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"testing"
    25  
    26  	"github.com/golang/mock/gomock"
    27  	"github.com/pingcap/failpoint"
    28  	"github.com/pingcap/tiflow/dm/checker"
    29  	"github.com/pingcap/tiflow/dm/config"
    30  	"github.com/pingcap/tiflow/dm/master/scheduler"
    31  	"github.com/pingcap/tiflow/dm/openapi"
    32  	"github.com/pingcap/tiflow/dm/openapi/fixtures"
    33  	"github.com/pingcap/tiflow/dm/pb"
    34  	"github.com/pingcap/tiflow/dm/pbmock"
    35  	"github.com/pingcap/tiflow/dm/pkg/ha"
    36  	"github.com/pingcap/tiflow/dm/pkg/log"
    37  	"github.com/pingcap/tiflow/dm/pkg/terror"
    38  	"github.com/stretchr/testify/suite"
    39  )
    40  
    41  type OpenAPIControllerSuite struct {
    42  	suite.Suite
    43  
    44  	testSource *openapi.Source
    45  	testTask   *openapi.Task
    46  }
    47  
    48  func (s *OpenAPIControllerSuite) SetupSuite() {
    49  	s.NoError(log.InitLogger(&log.Config{}))
    50  
    51  	dbCfg := config.GetDBConfigForTest()
    52  	s.testSource = &openapi.Source{
    53  		SourceName: source1Name,
    54  		Enable:     true,
    55  		EnableGtid: false,
    56  		Host:       dbCfg.Host,
    57  		Password:   &dbCfg.Password,
    58  		Port:       dbCfg.Port,
    59  		User:       dbCfg.User,
    60  	}
    61  
    62  	task, err := fixtures.GenNoShardOpenAPITaskForTest()
    63  	s.Nil(err)
    64  	s.testTask = &task
    65  
    66  	checker.CheckSyncConfigFunc = mockCheckSyncConfig
    67  	CheckAndAdjustSourceConfigFunc = checkAndNoAdjustSourceConfigMock
    68  	s.Nil(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB", `return(true)`))
    69  	s.Nil(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData", `return(true)`))
    70  }
    71  
    72  func (s *OpenAPIControllerSuite) TearDownSuite() {
    73  	CheckAndAdjustSourceConfigFunc = checkAndAdjustSourceConfig
    74  	checker.CheckSyncConfigFunc = checker.CheckSyncConfig
    75  	s.Nil(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB"))
    76  	s.Nil(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData"))
    77  }
    78  
    79  func (s *OpenAPIControllerSuite) TestSourceController() {
    80  	ctx, cancel := context.WithCancel(context.Background())
    81  	server := setupTestServer(ctx, s.T())
    82  	defer func() {
    83  		cancel()
    84  		server.Close()
    85  	}()
    86  
    87  	// reset omitted value after get
    88  	resetValue := func(source *openapi.Source) {
    89  		source.Purge = s.testSource.Purge
    90  		source.Password = s.testSource.Password
    91  		source.RelayConfig = s.testSource.RelayConfig
    92  	}
    93  
    94  	// create
    95  	{
    96  		createReq := openapi.CreateSourceRequest{
    97  			Source: *s.testSource,
    98  		}
    99  		source, err := server.createSource(ctx, createReq)
   100  		s.NoError(err)
   101  		s.EqualValues(s.testSource, source)
   102  	}
   103  
   104  	// update
   105  	{
   106  		source := *s.testSource
   107  		source.EnableGtid = !source.EnableGtid
   108  		updateReq := openapi.UpdateSourceRequest{Source: source}
   109  		sourceAfterUpdated, err := server.updateSource(ctx, source.SourceName, updateReq)
   110  		s.NoError(err)
   111  		s.Equal(source.EnableGtid, sourceAfterUpdated.EnableGtid)
   112  
   113  		// update back to continue next text
   114  		updateReq.Source = *s.testSource
   115  		sourceAfterUpdated, err = server.updateSource(ctx, source.SourceName, updateReq)
   116  		s.NoError(err)
   117  		s.EqualValues(s.testSource, sourceAfterUpdated)
   118  	}
   119  
   120  	// get and with status
   121  	{
   122  		withStatus := false
   123  		params := openapi.DMAPIGetSourceParams{WithStatus: &withStatus}
   124  		sourceAfterGet, err := server.getSource(ctx, s.testSource.SourceName, params)
   125  		s.NoError(err)
   126  
   127  		resetValue(sourceAfterGet)
   128  		s.EqualValues(s.testSource, sourceAfterGet)
   129  		s.Nil(sourceAfterGet.StatusList)
   130  
   131  		// with status
   132  		withStatus = true
   133  		params.WithStatus = &withStatus
   134  		sourceAfterGet, err = server.getSource(ctx, s.testSource.SourceName, params)
   135  		s.NoError(err)
   136  		s.NotNil(sourceAfterGet.StatusList)
   137  		statusList := *sourceAfterGet.StatusList
   138  		s.Len(statusList, 1)
   139  		s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg
   140  		s.Contains(*statusList[0].ErrorMsg, "code=38029")
   141  	}
   142  
   143  	// get status
   144  	{
   145  		statusList, err := server.getSourceStatus(ctx, s.testSource.SourceName)
   146  		s.NoError(err)
   147  		s.Len(statusList, 1)
   148  		s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg
   149  		s.Contains(*statusList[0].ErrorMsg, "code=38029")
   150  	}
   151  
   152  	// list and with status
   153  	{
   154  		withStatus := false
   155  		params := openapi.DMAPIGetSourceListParams{WithStatus: &withStatus}
   156  		sourceList, err := server.listSource(ctx, params)
   157  		s.NoError(err)
   158  		s.Len(sourceList, 1)
   159  		resetValue(&sourceList[0])
   160  		s.EqualValues(s.testSource, &sourceList[0])
   161  
   162  		withStatus = true
   163  		sourceList, err = server.listSource(ctx, params)
   164  		s.NoError(err)
   165  		s.Len(sourceList, 1)
   166  		s.NotNil(sourceList[0].StatusList)
   167  		statusList := *sourceList[0].StatusList
   168  		s.Contains(*statusList[0].ErrorMsg, "code=38029") // no worker, will get an error msg
   169  	}
   170  
   171  	// test with fake worker enable/disable/transfer/enable/disable/purge-relay
   172  	{
   173  		// enable on a free worker
   174  		s.True(terror.ErrWorkerNoStart.Equal(server.enableSource(ctx, s.testSource.SourceName))) // no free worker now
   175  
   176  		worker1Name := "worker1"
   177  		worker1Addr := "172.16.10.72:8262"
   178  		s.Nil(server.scheduler.AddWorker(worker1Name, worker1Addr))
   179  		worker1 := server.scheduler.GetWorkerByName(worker1Name)
   180  		worker1.ToFree()
   181  		s.NoError(server.transferSource(ctx, s.testSource.SourceName, worker1Name))
   182  		s.Equal(worker1.Stage(), scheduler.WorkerBound)
   183  
   184  		// disable no task now, so no error
   185  		s.NoError(server.disableSource(ctx, s.testSource.SourceName))
   186  
   187  		// add worker 2 and transfer
   188  		worker2Name := "worker2"
   189  		worker2Addr := "172.16.10.72:8263"
   190  		s.Nil(server.scheduler.AddWorker(worker2Name, worker2Addr))
   191  		worker2 := server.scheduler.GetWorkerByName(worker2Name)
   192  		worker2.ToFree()
   193  		s.NoError(server.transferSource(ctx, s.testSource.SourceName, worker2Name))
   194  		s.Equal(worker1.Stage(), scheduler.WorkerFree)
   195  		s.Equal(worker2.Stage(), scheduler.WorkerBound)
   196  
   197  		// enable relay with binlog name will trigger a update source
   198  		relayBinLogName := "mysql-bin.000001"
   199  		enableRelayReq := openapi.EnableRelayRequest{RelayBinlogName: &relayBinLogName}
   200  		s.NoError(server.enableRelay(ctx, s.testSource.SourceName, enableRelayReq))
   201  
   202  		updatedSource, err := server.getSource(ctx, s.testSource.SourceName, openapi.DMAPIGetSourceParams{})
   203  		s.NoError(err)
   204  		s.Equal(*updatedSource.RelayConfig.RelayBinlogName, relayBinLogName)
   205  
   206  		// disable relay
   207  		disableRelayReq := openapi.DisableRelayRequest{}
   208  		err = server.disableRelay(ctx, s.testSource.SourceName, disableRelayReq)
   209  		s.NoError(err)
   210  		updatedSource, err = server.getSource(ctx, s.testSource.SourceName, openapi.DMAPIGetSourceParams{})
   211  		s.NoError(err)
   212  		s.False(*updatedSource.RelayConfig.EnableRelay)
   213  
   214  		// purge
   215  		purgeRelayReq := openapi.PurgeRelayRequest{}
   216  		err = server.purgeRelay(ctx, s.testSource.SourceName, purgeRelayReq)
   217  		s.Error(err)
   218  		s.Regexp(".*please `enable-relay` first", err.Error())
   219  	}
   220  
   221  	// delete
   222  	{
   223  		s.Nil(server.deleteSource(ctx, s.testSource.SourceName, false))
   224  		sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{})
   225  		s.Nil(err)
   226  		s.Len(sourceList, 0)
   227  	}
   228  
   229  	// create and update no password source
   230  	{
   231  		// no password will use "" as password
   232  		source := *s.testSource
   233  		source.Password = nil
   234  		createReq := openapi.CreateSourceRequest{Source: source}
   235  		resp, err := server.createSource(ctx, createReq)
   236  		s.NoError(err)
   237  		s.EqualValues(source, *resp)
   238  		config := server.scheduler.GetSourceCfgByID(source.SourceName)
   239  		s.NotNil(config)
   240  		s.Equal("", config.From.Password)
   241  
   242  		// update to have password
   243  		updateReq := openapi.UpdateSourceRequest{Source: *s.testSource}
   244  		sourceAfterUpdated, err := server.updateSource(ctx, source.SourceName, updateReq)
   245  		s.NoError(err)
   246  		s.EqualValues(s.testSource, sourceAfterUpdated)
   247  
   248  		// update without password will use old password
   249  		source = *s.testSource
   250  		source.Password = nil
   251  		updateReq = openapi.UpdateSourceRequest{Source: source}
   252  		sourceAfterUpdated, err = server.updateSource(ctx, source.SourceName, updateReq)
   253  		s.NoError(err)
   254  		s.Equal(source, *sourceAfterUpdated)
   255  		// password is old
   256  		config = server.scheduler.GetSourceCfgByID(source.SourceName)
   257  		s.NotNil(config)
   258  		s.Equal(*s.testSource.Password, config.From.Password)
   259  
   260  		s.Nil(server.deleteSource(ctx, s.testSource.SourceName, false))
   261  	}
   262  }
   263  
   264  func (s *OpenAPIControllerSuite) TestTaskController() {
   265  	ctx, cancel := context.WithCancel(context.Background())
   266  	server := setupTestServer(ctx, s.T())
   267  	defer func() {
   268  		cancel()
   269  		server.Close()
   270  	}()
   271  
   272  	// create source for task
   273  	{
   274  		// add a mock worker
   275  		worker1Name := "worker1"
   276  		worker1Addr := "172.16.10.72:8262"
   277  		s.Nil(server.scheduler.AddWorker(worker1Name, worker1Addr))
   278  		worker1 := server.scheduler.GetWorkerByName(worker1Name)
   279  		worker1.ToFree()
   280  
   281  		_, err := server.createSource(ctx, openapi.CreateSourceRequest{Source: *s.testSource, WorkerName: &worker1Name})
   282  		s.Nil(err)
   283  		s.Equal(worker1.Stage(), scheduler.WorkerBound)
   284  		sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{})
   285  		s.Nil(err)
   286  		s.Len(sourceList, 1)
   287  	}
   288  
   289  	// create
   290  	{
   291  		createTaskReq := openapi.CreateTaskRequest{Task: *s.testTask}
   292  		res, err := server.createTask(ctx, createTaskReq)
   293  		s.Nil(err)
   294  		s.EqualValues(*s.testTask, res.Task)
   295  	}
   296  
   297  	// update
   298  	{
   299  		task := *s.testTask
   300  		batch := 1000
   301  		task.SourceConfig.IncrMigrateConf.ReplBatch = &batch
   302  		updateReq := openapi.UpdateTaskRequest{Task: task}
   303  		s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate", `return("success")`))
   304  		res, err := server.updateTask(ctx, updateReq)
   305  		s.NoError(err)
   306  		s.EqualValues(task.SourceConfig.IncrMigrateConf, res.Task.SourceConfig.IncrMigrateConf)
   307  
   308  		// update back to continue next text
   309  		updateReq.Task = *s.testTask
   310  		res, err = server.updateTask(ctx, updateReq)
   311  		s.NoError(err)
   312  		s.EqualValues(*s.testTask, res.Task)
   313  		s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate"))
   314  	}
   315  
   316  	// get and with status
   317  	{
   318  		withStatus := false
   319  		params := openapi.DMAPIGetTaskParams{WithStatus: &withStatus}
   320  		taskAfterGet, err := server.getTask(ctx, s.testTask.Name, params)
   321  		s.NoError(err)
   322  		s.EqualValues(s.testTask, taskAfterGet)
   323  		s.Nil(taskAfterGet.StatusList)
   324  
   325  		// with status
   326  		withStatus = true
   327  		params.WithStatus = &withStatus
   328  		taskAfterGet, err = server.getTask(ctx, s.testTask.Name, params)
   329  		s.NoError(err)
   330  		s.NotNil(taskAfterGet.StatusList)
   331  		statusList := *taskAfterGet.StatusList
   332  		s.Len(statusList, 1)
   333  		s.NotNil(statusList[0].ErrorMsg) // no true worker, will get an error msg
   334  		s.Contains(*statusList[0].ErrorMsg, "code=38008")
   335  	}
   336  
   337  	// get status
   338  	{
   339  		params := openapi.DMAPIGetTaskStatusParams{}
   340  		statusList, err := server.getTaskStatus(ctx, s.testTask.Name, params)
   341  		s.NoError(err)
   342  		s.Len(statusList, 1)
   343  		s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg
   344  		s.Contains(*statusList[0].ErrorMsg, "code=38008")
   345  	}
   346  
   347  	// list and with status
   348  	{
   349  		withStatus := false
   350  		params := openapi.DMAPIGetTaskListParams{WithStatus: &withStatus}
   351  		taskList, err := server.listTask(ctx, params)
   352  		s.NoError(err)
   353  		s.Len(taskList, 1)
   354  		s.EqualValues(s.testTask, &taskList[0])
   355  
   356  		withStatus = true
   357  		taskList, err = server.listTask(ctx, params)
   358  		s.NoError(err)
   359  		s.Len(taskList, 1)
   360  		s.NotNil(taskList[0].StatusList)
   361  		statusList := *taskList[0].StatusList
   362  		s.Contains(*statusList[0].ErrorMsg, "code=38008")
   363  	}
   364  
   365  	// start and stop
   366  	{
   367  		// can't start when source not enabled
   368  		req := openapi.StartTaskRequest{}
   369  		s.Nil(server.disableSource(ctx, s.testSource.SourceName))
   370  		s.True(terror.ErrMasterStartTask.Equal(server.startTask(ctx, s.testTask.Name, req)))
   371  		// start success
   372  		s.Nil(server.enableSource(ctx, s.testSource.SourceName))
   373  		s.Nil(server.startTask(ctx, s.testTask.Name, req))
   374  		s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Running)
   375  
   376  		// stop success
   377  		s.Nil(server.stopTask(ctx, s.testTask.Name, openapi.StopTaskRequest{}))
   378  		s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped)
   379  
   380  		// start with cli args
   381  		startTime := "2022-05-05 12:12:12"
   382  		safeModeTimeDuration := "10s"
   383  		req = openapi.StartTaskRequest{
   384  			StartTime:            &startTime,
   385  			SafeModeTimeDuration: &safeModeTimeDuration,
   386  		}
   387  		s.Nil(server.startTask(ctx, s.testTask.Name, req))
   388  		taskCliConf, err := ha.GetTaskCliArgs(server.etcdClient, s.testTask.Name, s.testSource.SourceName)
   389  		s.Nil(err)
   390  		s.NotNil(taskCliConf)
   391  		s.Equal(startTime, taskCliConf.StartTime)
   392  		s.Equal(safeModeTimeDuration, taskCliConf.SafeModeDuration)
   393  
   394  		// stop success
   395  		s.Nil(server.stopTask(ctx, s.testTask.Name, openapi.StopTaskRequest{}))
   396  		s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped)
   397  	}
   398  
   399  	// delete
   400  	{
   401  		s.Nil(server.deleteTask(ctx, s.testTask.Name, true)) // delete with fore
   402  		taskList, err := server.listTask(ctx, openapi.DMAPIGetTaskListParams{})
   403  		s.Nil(err)
   404  		s.Len(taskList, 0)
   405  	}
   406  
   407  	// convert between task and taskConfig
   408  	{
   409  		// from task to taskConfig
   410  		req := openapi.ConverterTaskRequest{Task: s.testTask}
   411  		task, taskCfg, err := server.convertTaskConfig(ctx, req)
   412  		s.NoError(err)
   413  		s.NotNil(task)
   414  		s.NotNil(taskCfg)
   415  		s.EqualValues(s.testTask, task)
   416  
   417  		// from taskCfg to task
   418  		req.Task = nil
   419  		taskCfgStr := taskCfg.String()
   420  		req.TaskConfigFile = &taskCfgStr
   421  		task2, taskCfg2, err := server.convertTaskConfig(ctx, req)
   422  		s.NoError(err)
   423  		s.NotNil(task2)
   424  		s.NotNil(taskCfg2)
   425  		s.EqualValues(task2, task)
   426  		s.Equal(taskCfg2.String(), taskCfg.String())
   427  
   428  		// incremental task without source meta
   429  		taskTest := *s.testTask
   430  		taskTest.TaskMode = config.ModeIncrement
   431  		req = openapi.ConverterTaskRequest{Task: &taskTest}
   432  		task3, taskCfg3, err := server.convertTaskConfig(ctx, req)
   433  		s.NoError(err)
   434  		s.NotNil(task3)
   435  		s.NotNil(taskCfg3)
   436  		s.EqualValues(&taskTest, task3)
   437  
   438  		req.Task = nil
   439  		taskCfgStr = taskCfg3.String()
   440  		req.TaskConfigFile = &taskCfgStr
   441  		task4, taskCfg4, err := server.convertTaskConfig(ctx, req)
   442  		s.NoError(err)
   443  		s.NotNil(task4)
   444  		s.NotNil(taskCfg4)
   445  		s.EqualValues(task4, task3)
   446  		s.Equal(taskCfg4.String(), taskCfg3.String())
   447  	}
   448  }
   449  
   450  func (s *OpenAPIControllerSuite) TestTaskControllerWithInvalidTask() {
   451  	ctx, cancel := context.WithCancel(context.Background())
   452  	server := setupTestServer(ctx, s.T())
   453  	defer func() {
   454  		cancel()
   455  		server.Close()
   456  	}()
   457  
   458  	// create an invalid task
   459  	task, err := fixtures.GenNoShardErrNameOpenAPITaskForTest()
   460  	s.NoError(err)
   461  
   462  	// create source for task
   463  	{
   464  		// add a mock worker
   465  		worker1Name := "worker1"
   466  		worker1Addr := "172.16.10.72:8262"
   467  		s.NoError(server.scheduler.AddWorker(worker1Name, worker1Addr))
   468  		worker1 := server.scheduler.GetWorkerByName(worker1Name)
   469  		worker1.ToFree()
   470  
   471  		// create the corresponding mock worker
   472  		ctrl := gomock.NewController(s.T())
   473  		defer ctrl.Finish()
   474  		mockWorkerClient := pbmock.NewMockWorkerClient(ctrl)
   475  		queryResp := &pb.QueryStatusResponse{
   476  			Result: true,
   477  			Msg:    "",
   478  			SourceStatus: &pb.SourceStatus{
   479  				Source: s.testSource.SourceName,
   480  				Worker: worker1Name,
   481  			},
   482  			SubTaskStatus: [](*pb.SubTaskStatus){&pb.SubTaskStatus{
   483  				Result: &pb.ProcessResult{
   484  					Errors: [](*pb.ProcessError){&pb.ProcessError{
   485  						ErrCode:  10006,
   486  						ErrClass: "database",
   487  						ErrScope: "downstream",
   488  						ErrLevel: "high",
   489  						Message:  fmt.Sprintf("fail to initialize unit Load of %s : execute statement failed: CREATE TABLE IF NOT EXISTS `dm_meta`.`%s` (\n\t\ttask_name varchar(255) NOT NULL,\n\t\tsource_name varchar(255) NOT NULL,\n\t\tstatus varchar(10) NOT NULL DEFAULT 'init' COMMENT 'init,running,finished',\n\t\tPRIMARY KEY (task_name, source_name)\n\t);\n", task.Name, task.Name),
   490  						RawCause: fmt.Sprintf("Error 1059: Identifier name '%s' is too long.", task.Name),
   491  					}},
   492  				},
   493  			}},
   494  		}
   495  		mockWorkerClient.EXPECT().QueryStatus(
   496  			gomock.Any(),
   497  			gomock.Any(),
   498  		).Return(queryResp, nil).MaxTimes(maxRetryNum)
   499  		server.scheduler.SetWorkerClientForTest(worker1Name, newMockRPCClient(mockWorkerClient))
   500  
   501  		_, err := server.createSource(ctx, openapi.CreateSourceRequest{Source: *s.testSource, WorkerName: &worker1Name})
   502  		s.NoError(err)
   503  		s.Equal(worker1.Stage(), scheduler.WorkerBound)
   504  		sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{})
   505  		s.NoError(err)
   506  		s.Len(sourceList, 1)
   507  	}
   508  
   509  	// create
   510  	{
   511  		createTaskReq := openapi.CreateTaskRequest{Task: task}
   512  		res, err := server.createTask(ctx, createTaskReq)
   513  		s.NoError(err)
   514  		s.EqualValues(task, res.Task)
   515  	}
   516  
   517  	// start and stop
   518  	{
   519  		// start success
   520  		req := openapi.StartTaskRequest{}
   521  		s.NoError(server.enableSource(ctx, s.testSource.SourceName))
   522  		s.NoError(server.startTask(ctx, task.Name, req))
   523  		s.Equal(server.scheduler.GetExpectSubTaskStage(task.Name, s.testSource.SourceName).Expect, pb.Stage_Running)
   524  
   525  		// get status
   526  		{
   527  			params := openapi.DMAPIGetTaskStatusParams{}
   528  			statusList, err := server.getTaskStatus(ctx, task.Name, params)
   529  			s.NoError(err)
   530  			s.Len(statusList, 1)
   531  			s.NotNil(statusList[0].ErrorMsg)
   532  			s.Contains(*statusList[0].ErrorMsg, "Error 1059: ") // database error, will return an error message
   533  		}
   534  
   535  		// stop success
   536  		s.NoError(server.stopTask(ctx, task.Name, openapi.StopTaskRequest{}))
   537  		s.Equal(server.scheduler.GetExpectSubTaskStage(task.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped)
   538  	}
   539  
   540  	// delete
   541  	{
   542  		s.NoError(server.deleteTask(ctx, task.Name, true)) // delete with fore
   543  		taskList, err := server.listTask(ctx, openapi.DMAPIGetTaskListParams{})
   544  		s.NoError(err)
   545  		s.Len(taskList, 0)
   546  	}
   547  }
   548  
   549  func TestOpenAPIControllerSuite(t *testing.T) {
   550  	suite.Run(t, new(OpenAPIControllerSuite))
   551  }