github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/jobmaster/dm/openapi_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  package dm
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"encoding/json"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"os"
    23  	"testing"
    24  
    25  	"github.com/DATA-DOG/go-sqlmock"
    26  	"github.com/gin-gonic/gin"
    27  	"github.com/pingcap/log"
    28  	"github.com/pingcap/tiflow/dm/checker"
    29  	dmconfig "github.com/pingcap/tiflow/dm/config"
    30  	dmmaster "github.com/pingcap/tiflow/dm/master"
    31  	"github.com/pingcap/tiflow/dm/pkg/conn"
    32  	"github.com/pingcap/tiflow/dm/pkg/terror"
    33  	"github.com/pingcap/tiflow/engine/jobmaster/dm/config"
    34  	"github.com/pingcap/tiflow/engine/jobmaster/dm/metadata"
    35  	"github.com/pingcap/tiflow/engine/jobmaster/dm/openapi"
    36  	dmpkg "github.com/pingcap/tiflow/engine/pkg/dm"
    37  	resModel "github.com/pingcap/tiflow/engine/pkg/externalresource/model"
    38  	"github.com/pingcap/tiflow/engine/pkg/meta/mock"
    39  	engineOpenAPI "github.com/pingcap/tiflow/engine/pkg/openapi"
    40  	"github.com/pingcap/tiflow/engine/pkg/promutil"
    41  	"github.com/pingcap/tiflow/pkg/errors"
    42  	tmock "github.com/stretchr/testify/mock"
    43  	"github.com/stretchr/testify/require"
    44  	"github.com/stretchr/testify/suite"
    45  )
    46  
    47  const (
    48  	baseURL = "/api/v1/jobs/job-id/"
    49  )
    50  
    51  func TestDMOpenAPISuite(t *testing.T) {
    52  	suite.Run(t, new(testDMOpenAPISuite))
    53  }
    54  
    55  type testDMOpenAPISuite struct {
    56  	suite.Suite
    57  	jm              *JobMaster
    58  	engine          *gin.Engine
    59  	messageAgent    *dmpkg.MockMessageAgent
    60  	checkpointAnget *MockCheckpointAgent
    61  	funcBackup      func(ctx context.Context, cfg *dmconfig.SourceConfig) error
    62  }
    63  
    64  func (t *testDMOpenAPISuite) SetupSuite() {
    65  	var (
    66  		mockBaseJobmaster   = &MockBaseJobmaster{t: t.T()}
    67  		mockMessageAgent    = &dmpkg.MockMessageAgent{}
    68  		mockCheckpointAgent = &MockCheckpointAgent{}
    69  		jm                  = &JobMaster{
    70  			BaseJobMaster:   mockBaseJobmaster,
    71  			metadata:        metadata.NewMetaData(mock.NewMetaMock(), log.L()),
    72  			messageAgent:    mockMessageAgent,
    73  			checkpointAgent: mockCheckpointAgent,
    74  		}
    75  	)
    76  	jm.taskManager = NewTaskManager("test-job", nil, jm.metadata.JobStore(), jm.messageAgent, jm.Logger(), promutil.NewFactory4Test(t.T().TempDir()))
    77  	jm.workerManager = NewWorkerManager(mockBaseJobmaster.ID(), nil, jm.metadata.JobStore(), jm.metadata.UnitStateStore(), nil, jm.messageAgent, nil, jm.Logger(),
    78  		resModel.ResourceTypeLocalFile)
    79  	jm.initialized.Store(true)
    80  
    81  	engine := gin.New()
    82  	apiGroup := engine.Group(baseURL)
    83  	jm.initOpenAPI(apiGroup)
    84  
    85  	t.funcBackup = dmmaster.CheckAndAdjustSourceConfigFunc
    86  	t.jm = jm
    87  	t.engine = engine
    88  	t.messageAgent = mockMessageAgent
    89  	t.checkpointAnget = mockCheckpointAgent
    90  	dmmaster.CheckAndAdjustSourceConfigFunc = checkAndNoAdjustSourceConfigMock
    91  }
    92  
    93  func (t *testDMOpenAPISuite) TearDownSuite() {
    94  	dmmaster.CheckAndAdjustSourceConfigFunc = t.funcBackup
    95  }
    96  
    97  func (t *testDMOpenAPISuite) TestDMAPIGetJobConfig() {
    98  	w := httptest.NewRecorder()
    99  	r := httptest.NewRequest("GET", "/api/v1/jobs/job-not-exist/config", nil)
   100  	t.engine.ServeHTTP(w, r)
   101  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   102  
   103  	w = httptest.NewRecorder()
   104  	r = httptest.NewRequest("GET", baseURL+"config", nil)
   105  	t.engine.ServeHTTP(w, r)
   106  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   107  	equalError(t.T(), "state not found", w.Body)
   108  
   109  	jobCfg := &config.JobCfg{}
   110  	require.NoError(t.T(), jobCfg.DecodeFile(jobTemplatePath))
   111  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Create, jobCfg, nil))
   112  	w = httptest.NewRecorder()
   113  	r = httptest.NewRequest("GET", baseURL+"config", nil)
   114  	t.engine.ServeHTTP(w, r)
   115  	require.Equal(t.T(), http.StatusOK, w.Code)
   116  
   117  	var cfgStr string
   118  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &cfgStr))
   119  	bs, err := jobCfg.Yaml()
   120  	require.NoError(t.T(), err)
   121  	require.Equal(t.T(), sortString(string(bs)), sortString(cfgStr))
   122  
   123  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Delete, nil, nil))
   124  }
   125  
   126  func (t *testDMOpenAPISuite) TestDMAPIUpdateJobCfg() {
   127  	w := httptest.NewRecorder()
   128  	r := httptest.NewRequest("PUT", "/api/v1/jobs/job-not-exist/config", nil)
   129  	t.engine.ServeHTTP(w, r)
   130  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   131  
   132  	req := openapi.UpdateJobConfigRequest{}
   133  	bs, err := json.Marshal(req)
   134  	require.NoError(t.T(), err)
   135  	w = httptest.NewRecorder()
   136  	r = httptest.NewRequest("PUT", baseURL+"config", bytes.NewReader(bs))
   137  	r.Header.Set("Content-Type", "application/json")
   138  	t.engine.ServeHTTP(w, r)
   139  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   140  
   141  	cfgBytes, err := os.ReadFile(jobTemplatePath)
   142  	require.NoError(t.T(), err)
   143  	req = openapi.UpdateJobConfigRequest{Config: string(cfgBytes)}
   144  	bs, err = json.Marshal(req)
   145  	require.NoError(t.T(), err)
   146  	w = httptest.NewRecorder()
   147  	r = httptest.NewRequest("PUT", baseURL+"config", bytes.NewReader(bs))
   148  	r.Header.Set("Content-Type", "application/json")
   149  	t.engine.ServeHTTP(w, r)
   150  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   151  
   152  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Create, &config.JobCfg{}, nil))
   153  	t.checkpointAnget.On("Update").Return(nil)
   154  	verDB := conn.InitVersionDB()
   155  	verDB.ExpectQuery("SHOW GLOBAL VARIABLES LIKE 'version'").WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}).
   156  		AddRow("version", "5.7.25-TiDB-v6.1.0"))
   157  	checker.CheckSyncConfigFunc = func(_ context.Context, _ []*dmconfig.SubTaskConfig, _, _ int64) (string, error) {
   158  		return "check pass", nil
   159  	}
   160  	w = httptest.NewRecorder()
   161  	r = httptest.NewRequest("PUT", baseURL+"config", bytes.NewReader(bs))
   162  	r.Header.Set("Content-Type", "application/json")
   163  	t.engine.ServeHTTP(w, r)
   164  	require.Equal(t.T(), http.StatusOK, w.Code)
   165  }
   166  
   167  func (t *testDMOpenAPISuite) TestDMAPIGetJobStatus() {
   168  	w := httptest.NewRecorder()
   169  	r := httptest.NewRequest("GET", "/api/v1/jobs/job-not-exist/status", nil)
   170  	t.engine.ServeHTTP(w, r)
   171  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   172  
   173  	w = httptest.NewRecorder()
   174  	r = httptest.NewRequest("GET", baseURL+"status", nil)
   175  	t.engine.ServeHTTP(w, r)
   176  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   177  	equalError(t.T(), "state not found", w.Body)
   178  
   179  	w = httptest.NewRecorder()
   180  	r = httptest.NewRequest("GET", baseURL+"status"+"?tasks=task", nil)
   181  	t.engine.ServeHTTP(w, r)
   182  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   183  	equalError(t.T(), "state not found", w.Body)
   184  
   185  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Create, &config.JobCfg{}, nil))
   186  	w = httptest.NewRecorder()
   187  	r = httptest.NewRequest("GET", baseURL+"status", nil)
   188  	t.engine.ServeHTTP(w, r)
   189  	require.Equal(t.T(), http.StatusOK, w.Code)
   190  
   191  	jobStatus := JobStatus{
   192  		JobID:      "dm-jobmaster-id",
   193  		TaskStatus: map[string]TaskStatus{},
   194  	}
   195  	var jobStatus2 JobStatus
   196  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &jobStatus2))
   197  	require.Equal(t.T(), jobStatus, jobStatus2)
   198  
   199  	w = httptest.NewRecorder()
   200  	r = httptest.NewRequest("GET", baseURL+"status"+"?tasks=task1&tasks=task2", nil)
   201  	t.engine.ServeHTTP(w, r)
   202  	require.Equal(t.T(), http.StatusOK, w.Code)
   203  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &jobStatus2))
   204  	require.Equal(t.T(), "task task1 for job not found", jobStatus2.TaskStatus["task1"].Status.ErrorMsg)
   205  	require.Equal(t.T(), "task task2 for job not found", jobStatus2.TaskStatus["task2"].Status.ErrorMsg)
   206  
   207  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Delete, nil, nil))
   208  }
   209  
   210  func (t *testDMOpenAPISuite) TestDMAPIOperateJob() {
   211  	w := httptest.NewRecorder()
   212  	r := httptest.NewRequest("PUT", "/api/v1/jobs/job-not-exist/status", nil)
   213  	t.engine.ServeHTTP(w, r)
   214  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   215  
   216  	w = httptest.NewRecorder()
   217  	r = httptest.NewRequest("PUT", baseURL+"status", nil)
   218  	t.engine.ServeHTTP(w, r)
   219  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   220  	equalError(t.T(), "unsupported op type '' for operate task", w.Body)
   221  
   222  	tasks := []string{"task"}
   223  	req := openapi.OperateJobRequest{
   224  		Op:    openapi.OperateJobRequestOpPause,
   225  		Tasks: &tasks,
   226  	}
   227  	bs, err := json.Marshal(req)
   228  	require.NoError(t.T(), err)
   229  	w = httptest.NewRecorder()
   230  	r = httptest.NewRequest("PUT", baseURL+"status", bytes.NewReader(bs))
   231  	r.Header.Set("Content-Type", "application/json")
   232  	t.engine.ServeHTTP(w, r)
   233  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   234  	equalError(t.T(), "state not found", w.Body)
   235  
   236  	jobCfg := &config.JobCfg{}
   237  	require.NoError(t.T(), jobCfg.DecodeFile(jobTemplatePath))
   238  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Create, jobCfg, nil))
   239  	req.Op = openapi.OperateJobRequestOpResume
   240  	req.Tasks = nil
   241  	bs, err = json.Marshal(req)
   242  	require.NoError(t.T(), err)
   243  	w = httptest.NewRecorder()
   244  	r = httptest.NewRequest("PUT", baseURL+"status", bytes.NewReader(bs))
   245  	r.Header.Set("Content-Type", "application/json")
   246  	t.engine.ServeHTTP(w, r)
   247  	require.Equal(t.T(), http.StatusOK, w.Code)
   248  
   249  	require.NoError(t.T(), t.jm.taskManager.OperateTask(context.Background(), dmpkg.Delete, nil, nil))
   250  }
   251  
   252  func (t *testDMOpenAPISuite) TestDMAPIGetBinlogOperator() {
   253  	w := httptest.NewRecorder()
   254  	r := httptest.NewRequest("GET", "/api/v1/jobs/job-not-exist/binlog", nil)
   255  	t.engine.ServeHTTP(w, r)
   256  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   257  
   258  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(nil, errors.New("binlog operator not found")).Once()
   259  	w = httptest.NewRecorder()
   260  	r = httptest.NewRequest("GET", baseURL+"binlog/tasks/"+"task1"+"?binlog_pos='mysql-bin.000001,4'", nil)
   261  	t.engine.ServeHTTP(w, r)
   262  	require.Equal(t.T(), http.StatusOK, w.Code)
   263  	var binlogResp dmpkg.BinlogResponse
   264  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogResp))
   265  	require.Equal(t.T(), "", binlogResp.ErrorMsg)
   266  	require.Equal(t.T(), "binlog operator not found", binlogResp.Results["task1"].ErrorMsg)
   267  }
   268  
   269  func (t *testDMOpenAPISuite) TestDMAPISetBinlogOperator() {
   270  	w := httptest.NewRecorder()
   271  	r := httptest.NewRequest("POST", "/api/v1/jobs/job-not-exist/binlog", nil)
   272  	t.engine.ServeHTTP(w, r)
   273  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   274  
   275  	req := openapi.SetBinlogOperatorRequest{
   276  		Op: "wrong-op",
   277  	}
   278  	bs, err := json.Marshal(req)
   279  	require.NoError(t.T(), err)
   280  	w = httptest.NewRecorder()
   281  	r = httptest.NewRequest("POST", baseURL+"binlog/tasks/"+"task1", bytes.NewReader(bs))
   282  	r.Header.Set("Content-Type", "application/json")
   283  	t.engine.ServeHTTP(w, r)
   284  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   285  	equalError(t.T(), "unsupported op type 'wrong-op' for set binlog operator", w.Body)
   286  
   287  	pos := "mysql-bin.000001,4"
   288  	sqls := []string{"ALTER TABLE tb ADD COLUMN c int(11) UNIQUE;"}
   289  	req = openapi.SetBinlogOperatorRequest{
   290  		BinlogPos: &pos,
   291  		Op:        openapi.SetBinlogOperatorRequestOpReplace,
   292  		Sqls:      &sqls,
   293  	}
   294  	bs, err = json.Marshal(req)
   295  	require.NoError(t.T(), err)
   296  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   297  		Msg: "binlog operator set success",
   298  	}, nil).Once()
   299  	w = httptest.NewRecorder()
   300  	r = httptest.NewRequest("POST", baseURL+"binlog/tasks/"+"task1", bytes.NewReader(bs))
   301  	r.Header.Set("Content-Type", "application/json")
   302  	t.engine.ServeHTTP(w, r)
   303  	require.Equal(t.T(), http.StatusCreated, w.Code)
   304  	var binlogResp dmpkg.BinlogResponse
   305  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogResp))
   306  	require.Equal(t.T(), "", binlogResp.ErrorMsg)
   307  	require.Equal(t.T(), "binlog operator set success", binlogResp.Results["task1"].Msg)
   308  
   309  	req = openapi.SetBinlogOperatorRequest{
   310  		Op: openapi.SetBinlogOperatorRequestOpSkip,
   311  	}
   312  	bs, err = json.Marshal(req)
   313  	require.NoError(t.T(), err)
   314  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   315  		ErrorMsg: "no binlog error",
   316  	}, nil).Once()
   317  	w = httptest.NewRecorder()
   318  	r = httptest.NewRequest("POST", baseURL+"binlog/tasks/"+"task1", bytes.NewReader(bs))
   319  	r.Header.Set("Content-Type", "application/json")
   320  	t.engine.ServeHTTP(w, r)
   321  	require.Equal(t.T(), http.StatusCreated, w.Code)
   322  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogResp))
   323  	require.Equal(t.T(), "", binlogResp.ErrorMsg)
   324  	require.Equal(t.T(), "no binlog error", binlogResp.Results["task1"].ErrorMsg)
   325  
   326  	req = openapi.SetBinlogOperatorRequest{
   327  		Op: openapi.SetBinlogOperatorRequestOpInject,
   328  	}
   329  	bs, err = json.Marshal(req)
   330  	require.NoError(t.T(), err)
   331  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   332  		ErrorMsg: "no binlog error",
   333  	}, nil).Once()
   334  	w = httptest.NewRecorder()
   335  	r = httptest.NewRequest("POST", baseURL+"binlog/tasks/"+"task1", bytes.NewReader(bs))
   336  	r.Header.Set("Content-Type", "application/json")
   337  	t.engine.ServeHTTP(w, r)
   338  	require.Equal(t.T(), http.StatusCreated, w.Code)
   339  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogResp))
   340  	require.Equal(t.T(), "", binlogResp.ErrorMsg)
   341  	require.Equal(t.T(), "no binlog error", binlogResp.Results["task1"].ErrorMsg)
   342  }
   343  
   344  func (t *testDMOpenAPISuite) TestDMAPIDeleteBinlogOperator() {
   345  	w := httptest.NewRecorder()
   346  	r := httptest.NewRequest("DELETE", "/api/v1/jobs/job-not-exist/binlog", nil)
   347  	t.engine.ServeHTTP(w, r)
   348  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   349  
   350  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(nil, errors.New("binlog operator not found")).Once()
   351  	w = httptest.NewRecorder()
   352  	r = httptest.NewRequest("DELETE", baseURL+"binlog/tasks/"+"task1"+"?binlog_pos='mysql-bin.000001,4'", nil)
   353  	t.engine.ServeHTTP(w, r)
   354  	require.Equal(t.T(), http.StatusOK, w.Code)
   355  	var binlogResp dmpkg.BinlogResponse
   356  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogResp))
   357  	require.Equal(t.T(), "", binlogResp.ErrorMsg)
   358  	require.Equal(t.T(), "binlog operator not found", binlogResp.Results["task1"].ErrorMsg)
   359  
   360  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{}, nil).Once()
   361  	w = httptest.NewRecorder()
   362  	r = httptest.NewRequest("DELETE", baseURL+"binlog/tasks/"+"task1"+"?binlog_pos='mysql-bin.000001,4'", nil)
   363  	t.engine.ServeHTTP(w, r)
   364  	require.Equal(t.T(), http.StatusNoContent, w.Code)
   365  }
   366  
   367  func (t *testDMOpenAPISuite) TestDMAPIGetSchema() {
   368  	w := httptest.NewRecorder()
   369  	r := httptest.NewRequest("GET", "/api/v1/jobs/job-not-exist/schema", nil)
   370  	t.engine.ServeHTTP(w, r)
   371  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   372  
   373  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   374  		Msg: "targets",
   375  	}, nil).Once()
   376  	w = httptest.NewRecorder()
   377  	r = httptest.NewRequest("GET", baseURL+"schema/tasks/"+"task1"+"?target=true", nil)
   378  	t.engine.ServeHTTP(w, r)
   379  	require.Equal(t.T(), http.StatusOK, w.Code)
   380  	var binlogSchemaResp dmpkg.BinlogSchemaResponse
   381  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   382  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   383  	require.Equal(t.T(), "targets", binlogSchemaResp.Results["task1"].Msg)
   384  
   385  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   386  		Msg: "tables",
   387  	}, nil).Once()
   388  	w = httptest.NewRecorder()
   389  	r = httptest.NewRequest("GET", baseURL+"schema/tasks/"+"task1"+"?database='db'", nil)
   390  	t.engine.ServeHTTP(w, r)
   391  	require.Equal(t.T(), http.StatusOK, w.Code)
   392  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   393  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   394  	require.Equal(t.T(), "tables", binlogSchemaResp.Results["task1"].Msg)
   395  
   396  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   397  		Msg: "table",
   398  	}, nil).Once()
   399  	w = httptest.NewRecorder()
   400  	r = httptest.NewRequest("GET", baseURL+"schema/tasks/"+"task1"+"?database='db'&table='tb'", nil)
   401  	t.engine.ServeHTTP(w, r)
   402  	require.Equal(t.T(), http.StatusOK, w.Code)
   403  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   404  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   405  	require.Equal(t.T(), "table", binlogSchemaResp.Results["task1"].Msg)
   406  
   407  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   408  		Msg: "databases",
   409  	}, nil).Once()
   410  	w = httptest.NewRecorder()
   411  	r = httptest.NewRequest("GET", baseURL+"schema/tasks/"+"task1", nil)
   412  	t.engine.ServeHTTP(w, r)
   413  	require.Equal(t.T(), http.StatusOK, w.Code)
   414  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   415  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   416  	require.Equal(t.T(), "databases", binlogSchemaResp.Results["task1"].Msg)
   417  
   418  	w = httptest.NewRecorder()
   419  	r = httptest.NewRequest("GET", baseURL+"schema/tasks/"+"task1"+"?table='tb'", nil)
   420  	t.engine.ServeHTTP(w, r)
   421  	require.Equal(t.T(), http.StatusInternalServerError, w.Code)
   422  	equalError(t.T(), "invalid query params for get schema", w.Body)
   423  }
   424  
   425  func (t *testDMOpenAPISuite) TestDMAPISetSchema() {
   426  	w := httptest.NewRecorder()
   427  	r := httptest.NewRequest("PUT", "/api/v1/jobs/job-not-exist/schema", nil)
   428  	t.engine.ServeHTTP(w, r)
   429  	require.Equal(t.T(), http.StatusNotFound, w.Code)
   430  
   431  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(nil, errors.New("task not paused")).Once()
   432  	from := true
   433  	req := openapi.SetBinlogSchemaRequest{Database: "db", Table: "tb", FromSource: &from}
   434  	bs, err := json.Marshal(req)
   435  	require.NoError(t.T(), err)
   436  	w = httptest.NewRecorder()
   437  	r = httptest.NewRequest("PUT", baseURL+"schema/tasks/"+"task1", bytes.NewReader(bs))
   438  	r.Header.Set("Content-Type", "application/json")
   439  	t.engine.ServeHTTP(w, r)
   440  	require.Equal(t.T(), http.StatusOK, w.Code)
   441  	var binlogSchemaResp dmpkg.BinlogSchemaResponse
   442  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   443  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   444  	require.Equal(t.T(), "task not paused", binlogSchemaResp.Results["task1"].ErrorMsg)
   445  
   446  	t.messageAgent.On("SendRequest", tmock.Anything, tmock.Anything, tmock.Anything, tmock.Anything).Return(&dmpkg.CommonTaskResponse{
   447  		Msg: "success",
   448  	}, nil).Once()
   449  	req = openapi.SetBinlogSchemaRequest{Database: "db", Table: "tb", FromTarget: &from}
   450  	bs, err = json.Marshal(req)
   451  	require.NoError(t.T(), err)
   452  	w = httptest.NewRecorder()
   453  	r = httptest.NewRequest("PUT", baseURL+"schema/tasks/"+"task1", bytes.NewReader(bs))
   454  	r.Header.Set("Content-Type", "application/json")
   455  	t.engine.ServeHTTP(w, r)
   456  	require.Equal(t.T(), http.StatusOK, w.Code)
   457  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &binlogSchemaResp))
   458  	require.Equal(t.T(), "", binlogSchemaResp.ErrorMsg)
   459  	require.Equal(t.T(), "", binlogSchemaResp.Results["task1"].ErrorMsg)
   460  	require.Equal(t.T(), "success", binlogSchemaResp.Results["task1"].Msg)
   461  }
   462  
   463  func (t *testDMOpenAPISuite) TestJobMasterNotInitialized() {
   464  	t.jm.initialized.Store(false)
   465  	defer t.jm.initialized.Store(true)
   466  
   467  	w := httptest.NewRecorder()
   468  	r := httptest.NewRequest("GET", baseURL+"config", nil)
   469  	t.engine.ServeHTTP(w, r)
   470  	require.Equal(t.T(), errors.HTTPStatusCode(errors.ErrJobNotRunning), w.Code)
   471  	var httpErr engineOpenAPI.HTTPError
   472  	require.NoError(t.T(), json.Unmarshal(w.Body.Bytes(), &httpErr))
   473  	require.Equal(t.T(), string(errors.ErrJobNotRunning.RFCCode()), httpErr.Code)
   474  }
   475  
   476  func equalError(t *testing.T, expected string, body *bytes.Buffer) {
   477  	var httpErr engineOpenAPI.HTTPError
   478  	json.Unmarshal(body.Bytes(), &httpErr)
   479  	require.Equal(t, expected, httpErr.Message)
   480  }
   481  
   482  func TestHTTPErrorHandler(t *testing.T) {
   483  	t.Parallel()
   484  
   485  	testCases := []struct {
   486  		err     error
   487  		code    string
   488  		message string
   489  	}{
   490  		{
   491  			errors.New("unknown error"),
   492  			string(errors.ErrUnknown.RFCCode()),
   493  			"unknown error",
   494  		},
   495  		{
   496  			errors.ErrDeserializeConfig.GenWithStackByArgs(),
   497  			string(errors.ErrDeserializeConfig.RFCCode()),
   498  			errors.ErrDeserializeConfig.GetMsg(),
   499  		},
   500  		{
   501  			terror.ErrDBBadConn.Generate(),
   502  			"DM:ErrDBBadConn",
   503  			terror.ErrDBBadConn.Generate().Error(),
   504  		},
   505  		{
   506  			terror.ErrDBInvalidConn.Generate(),
   507  			"DM:ErrDBInvalidConn",
   508  			terror.ErrDBInvalidConn.Generate().Error(),
   509  		},
   510  	}
   511  
   512  	for _, tc := range testCases {
   513  		engine := gin.New()
   514  		engine.Use(httpErrorHandler())
   515  		engine.GET("/test", func(c *gin.Context) {
   516  			c.Error(tc.err)
   517  		})
   518  
   519  		w := httptest.NewRecorder()
   520  		r := httptest.NewRequest("GET", "/test", nil)
   521  		engine.ServeHTTP(w, r)
   522  		require.Equal(t, errors.HTTPStatusCode(tc.err), w.Code)
   523  
   524  		var httpErr engineOpenAPI.HTTPError
   525  		err := json.Unmarshal(w.Body.Bytes(), &httpErr)
   526  		require.NoError(t, err)
   527  		require.Equal(t, tc.code, httpErr.Code)
   528  		require.Equal(t, tc.message, httpErr.Message)
   529  	}
   530  }