github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/config/sink_test.go (about)

     1  // Copyright 2021 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 config
    15  
    16  import (
    17  	"net/url"
    18  	"testing"
    19  
    20  	"github.com/pingcap/tiflow/pkg/util"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func TestValidateTxnAtomicity(t *testing.T) {
    25  	t.Parallel()
    26  	testCases := []struct {
    27  		sinkURI        string
    28  		expectedErr    string
    29  		shouldSplitTxn bool
    30  	}{
    31  		{
    32  			sinkURI:        "mysql://normal:123456@127.0.0.1:3306",
    33  			expectedErr:    "",
    34  			shouldSplitTxn: true,
    35  		},
    36  		{
    37  			sinkURI:        "mysql://normal:123456@127.0.0.1:3306?transaction-atomicity=table",
    38  			expectedErr:    "",
    39  			shouldSplitTxn: false,
    40  		},
    41  		{
    42  			sinkURI:        "mysql://normal:123456@127.0.0.1:3306?transaction-atomicity=none",
    43  			expectedErr:    "",
    44  			shouldSplitTxn: true,
    45  		},
    46  		{
    47  			sinkURI:     "mysql://normal:123456@127.0.0.1:3306?transaction-atomicity=global",
    48  			expectedErr: "global level atomicity is not supported by.*",
    49  		},
    50  		{
    51  			sinkURI:     "tidb://normal:123456@127.0.0.1:3306?protocol=canal",
    52  			expectedErr: ".*protocol canal is incompatible with tidb scheme.*",
    53  		},
    54  		{
    55  			sinkURI:     "tidb://normal:123456@127.0.0.1:3306?protocol=default",
    56  			expectedErr: ".*protocol default is incompatible with tidb scheme.*",
    57  		},
    58  		{
    59  			sinkURI:     "tidb://normal:123456@127.0.0.1:3306?protocol=random",
    60  			expectedErr: ".*protocol .* is incompatible with tidb scheme.*",
    61  		},
    62  		{
    63  			sinkURI:        "blackhole://normal:123456@127.0.0.1:3306?transaction-atomicity=none",
    64  			expectedErr:    "",
    65  			shouldSplitTxn: true,
    66  		},
    67  		{
    68  			sinkURI: "kafka://127.0.0.1:9092?transaction-atomicity=none" +
    69  				"&protocol=open-protocol",
    70  			expectedErr:    "",
    71  			shouldSplitTxn: true,
    72  		},
    73  		{
    74  			sinkURI:        "kafka://127.0.0.1:9092?protocol=default",
    75  			expectedErr:    "",
    76  			shouldSplitTxn: true,
    77  		},
    78  		{
    79  			sinkURI:     "kafka://127.0.0.1:9092?transaction-atomicity=none",
    80  			expectedErr: ".*unknown .* message protocol for sink.*",
    81  		},
    82  		{
    83  			sinkURI: "kafka://127.0.0.1:9092?transaction-atomicity=table" +
    84  				"&protocol=open-protocol",
    85  			expectedErr: "table level atomicity is not supported by kafka scheme",
    86  		},
    87  		{
    88  			sinkURI: "kafka://127.0.0.1:9092?transaction-atomicity=invalid" +
    89  				"&protocol=open-protocol",
    90  			expectedErr: "invalid level atomicity is not supported by kafka scheme",
    91  		},
    92  		{
    93  			sinkURI: "pulsar://127.0.0.1:6550?transaction-atomicity=invalid" +
    94  				"&protocol=open-protocol",
    95  			expectedErr: "invalid level atomicity is not supported by pulsar scheme",
    96  		},
    97  		{
    98  			sinkURI:        "pulsar://127.0.0.1:6550/test?protocol=canal-json",
    99  			shouldSplitTxn: true,
   100  		},
   101  	}
   102  
   103  	for _, tc := range testCases {
   104  		cfg := SinkConfig{}
   105  		parsedSinkURI, err := url.Parse(tc.sinkURI)
   106  		require.Nil(t, err)
   107  		if tc.expectedErr == "" {
   108  			require.Nil(t, cfg.validateAndAdjust(parsedSinkURI))
   109  			require.Equal(t, tc.shouldSplitTxn, util.GetOrZero(cfg.TxnAtomicity).ShouldSplitTxn())
   110  		} else {
   111  			require.Regexp(t, tc.expectedErr, cfg.validateAndAdjust(parsedSinkURI))
   112  		}
   113  	}
   114  }
   115  
   116  func TestValidateProtocol(t *testing.T) {
   117  	t.Parallel()
   118  	testCases := []struct {
   119  		sinkConfig *SinkConfig
   120  		sinkURI    string
   121  		result     string
   122  	}{
   123  		{
   124  			sinkConfig: &SinkConfig{
   125  				Protocol: util.AddressOf("default"),
   126  			},
   127  			sinkURI: "kafka://127.0.0.1:9092?protocol=whatever",
   128  			result:  "whatever",
   129  		},
   130  		{
   131  			sinkConfig: &SinkConfig{},
   132  			sinkURI:    "kafka://127.0.0.1:9092?protocol=default",
   133  			result:     "default",
   134  		},
   135  		{
   136  			sinkConfig: &SinkConfig{
   137  				Protocol: util.AddressOf("default"),
   138  			},
   139  			sinkURI: "kafka://127.0.0.1:9092",
   140  			result:  "default",
   141  		},
   142  		{
   143  			sinkConfig: &SinkConfig{
   144  				Protocol: util.AddressOf("default"),
   145  			},
   146  			sinkURI: "pulsar://127.0.0.1:6650",
   147  			result:  "default",
   148  		},
   149  		{
   150  			sinkConfig: &SinkConfig{
   151  				Protocol: util.AddressOf("canal-json"),
   152  			},
   153  			sinkURI: "pulsar://127.0.0.1:6650/test?protocol=canal-json",
   154  			result:  "canal-json",
   155  		},
   156  	}
   157  	for _, c := range testCases {
   158  		parsedSinkURI, err := url.Parse(c.sinkURI)
   159  		require.Nil(t, err)
   160  		c.sinkConfig.validateAndAdjustSinkURI(parsedSinkURI)
   161  		require.Equal(t, c.result, util.GetOrZero(c.sinkConfig.Protocol))
   162  	}
   163  }
   164  
   165  func TestApplyParameterBySinkURI(t *testing.T) {
   166  	t.Parallel()
   167  	kafkaURI := "kafka://127.0.0.1:9092?protocol=whatever&transaction-atomicity=none"
   168  	testCases := []struct {
   169  		sinkConfig           *SinkConfig
   170  		sinkURI              string
   171  		expectedErr          string
   172  		expectedProtocol     string
   173  		expectedTxnAtomicity AtomicityLevel
   174  	}{
   175  		// test only config file
   176  		{
   177  			sinkConfig: &SinkConfig{
   178  				Protocol:     util.AddressOf("default"),
   179  				TxnAtomicity: util.AddressOf(noneTxnAtomicity),
   180  			},
   181  			sinkURI:              "kafka://127.0.0.1:9092",
   182  			expectedProtocol:     "default",
   183  			expectedTxnAtomicity: noneTxnAtomicity,
   184  		},
   185  		// test only sink uri
   186  		{
   187  			sinkConfig:           &SinkConfig{},
   188  			sinkURI:              kafkaURI,
   189  			expectedProtocol:     "whatever",
   190  			expectedTxnAtomicity: noneTxnAtomicity,
   191  		},
   192  		// test conflict scenarios
   193  		{
   194  			sinkConfig: &SinkConfig{
   195  				Protocol:     util.AddressOf("default"),
   196  				TxnAtomicity: util.AddressOf(tableTxnAtomicity),
   197  			},
   198  			sinkURI:              kafkaURI,
   199  			expectedProtocol:     "whatever",
   200  			expectedTxnAtomicity: noneTxnAtomicity,
   201  			expectedErr:          "incompatible configuration in sink uri",
   202  		},
   203  		{
   204  			sinkConfig: &SinkConfig{
   205  				Protocol:     util.AddressOf("default"),
   206  				TxnAtomicity: util.AddressOf(unknownTxnAtomicity),
   207  			},
   208  			sinkURI:              kafkaURI,
   209  			expectedProtocol:     "whatever",
   210  			expectedTxnAtomicity: noneTxnAtomicity,
   211  			expectedErr:          "incompatible configuration in sink uri",
   212  		},
   213  	}
   214  	for _, tc := range testCases {
   215  		parsedSinkURI, err := url.Parse(tc.sinkURI)
   216  		require.Nil(t, err)
   217  		err = tc.sinkConfig.applyParameterBySinkURI(parsedSinkURI)
   218  
   219  		require.Equal(t, util.AddressOf(tc.expectedProtocol), tc.sinkConfig.Protocol)
   220  		require.Equal(t, util.AddressOf(tc.expectedTxnAtomicity), tc.sinkConfig.TxnAtomicity)
   221  		if tc.expectedErr == "" {
   222  			require.NoError(t, err)
   223  		} else {
   224  			require.ErrorContains(t, err, tc.expectedErr)
   225  		}
   226  	}
   227  }
   228  
   229  func TestCheckCompatibilityWithSinkURI(t *testing.T) {
   230  	t.Parallel()
   231  	testCases := []struct {
   232  		newSinkConfig        *SinkConfig
   233  		oldSinkConfig        *SinkConfig
   234  		newsinkURI           string
   235  		expectedErr          string
   236  		expectedProtocol     *string
   237  		expectedTxnAtomicity *AtomicityLevel
   238  	}{
   239  		// test no update
   240  		{
   241  			newSinkConfig:        &SinkConfig{},
   242  			oldSinkConfig:        &SinkConfig{},
   243  			newsinkURI:           "kafka://",
   244  			expectedProtocol:     nil,
   245  			expectedTxnAtomicity: nil,
   246  		},
   247  		// test update config return err
   248  		{
   249  			newSinkConfig: &SinkConfig{
   250  				TxnAtomicity: util.AddressOf(tableTxnAtomicity),
   251  			},
   252  			oldSinkConfig: &SinkConfig{
   253  				TxnAtomicity: util.AddressOf(noneTxnAtomicity),
   254  			},
   255  			newsinkURI:           "kafka://127.0.0.1:9092?transaction-atomicity=none",
   256  			expectedErr:          "incompatible configuration in sink uri",
   257  			expectedProtocol:     nil,
   258  			expectedTxnAtomicity: util.AddressOf(noneTxnAtomicity),
   259  		},
   260  		// test update compatible config
   261  		{
   262  			newSinkConfig: &SinkConfig{
   263  				Protocol: util.AddressOf("canal"),
   264  			},
   265  			oldSinkConfig: &SinkConfig{
   266  				TxnAtomicity: util.AddressOf(noneTxnAtomicity),
   267  			},
   268  			newsinkURI:           "kafka://127.0.0.1:9092?transaction-atomicity=none",
   269  			expectedProtocol:     util.AddressOf("canal"),
   270  			expectedTxnAtomicity: util.AddressOf(noneTxnAtomicity),
   271  		},
   272  		// test update sinkuri
   273  		{
   274  			newSinkConfig: &SinkConfig{
   275  				TxnAtomicity: util.AddressOf(noneTxnAtomicity),
   276  			},
   277  			oldSinkConfig: &SinkConfig{
   278  				TxnAtomicity: util.AddressOf(noneTxnAtomicity),
   279  			},
   280  			newsinkURI:           "kafka://127.0.0.1:9092?transaction-atomicity=table",
   281  			expectedProtocol:     nil,
   282  			expectedTxnAtomicity: util.AddressOf(tableTxnAtomicity),
   283  		},
   284  	}
   285  	for _, tc := range testCases {
   286  		err := tc.newSinkConfig.CheckCompatibilityWithSinkURI(tc.oldSinkConfig, tc.newsinkURI)
   287  		if tc.expectedErr == "" {
   288  			require.NoError(t, err)
   289  		} else {
   290  			require.ErrorContains(t, err, tc.expectedErr)
   291  		}
   292  		require.Equal(t, tc.expectedProtocol, tc.newSinkConfig.Protocol)
   293  		require.Equal(t, tc.expectedTxnAtomicity, tc.newSinkConfig.TxnAtomicity)
   294  	}
   295  }
   296  
   297  func TestValidateAndAdjustCSVConfig(t *testing.T) {
   298  	t.Parallel()
   299  	tests := []struct {
   300  		name    string
   301  		config  *CSVConfig
   302  		wantErr string
   303  	}{
   304  		{
   305  			name: "valid quote",
   306  			config: &CSVConfig{
   307  				Quote:                "\"",
   308  				Delimiter:            ",",
   309  				BinaryEncodingMethod: BinaryEncodingBase64,
   310  			},
   311  			wantErr: "",
   312  		},
   313  		{
   314  			name: "quote has multiple characters",
   315  			config: &CSVConfig{
   316  				Quote: "***",
   317  			},
   318  			wantErr: "csv config quote contains more than one character",
   319  		},
   320  		{
   321  			name: "quote contains line break character",
   322  			config: &CSVConfig{
   323  				Quote: "\n",
   324  			},
   325  			wantErr: "csv config quote cannot be line break character",
   326  		},
   327  		{
   328  			name: "valid delimiter1",
   329  			config: &CSVConfig{
   330  				Quote:                "\"",
   331  				Delimiter:            ",",
   332  				BinaryEncodingMethod: BinaryEncodingHex,
   333  			},
   334  			wantErr: "",
   335  		},
   336  		{
   337  			name: "valid delimiter with 2 characters",
   338  			config: &CSVConfig{
   339  				Quote:                "\"",
   340  				Delimiter:            "FE",
   341  				BinaryEncodingMethod: BinaryEncodingHex,
   342  			},
   343  			wantErr: "",
   344  		},
   345  		{
   346  			name: "valid delimiter with 3 characters",
   347  			config: &CSVConfig{
   348  				Quote:                "\"",
   349  				Delimiter:            "|@|",
   350  				BinaryEncodingMethod: BinaryEncodingHex,
   351  			},
   352  			wantErr: "",
   353  		},
   354  		{
   355  			name: "delimiter is empty",
   356  			config: &CSVConfig{
   357  				Quote:     "'",
   358  				Delimiter: "",
   359  			},
   360  			wantErr: "csv config delimiter cannot be empty",
   361  		},
   362  		{
   363  			name: "delimiter contains line break character",
   364  			config: &CSVConfig{
   365  				Quote:     "'",
   366  				Delimiter: "\r",
   367  			},
   368  			wantErr: "csv config delimiter contains line break characters",
   369  		},
   370  		{
   371  			name: "delimiter contains more than three characters",
   372  			config: &CSVConfig{
   373  				Quote:     "'",
   374  				Delimiter: "FEFA",
   375  			},
   376  			wantErr: "csv config delimiter contains more than three characters, note that escape " +
   377  				"sequences can only be used in double quotes in toml configuration items.",
   378  		},
   379  		{
   380  			name: "delimiter and quote are same",
   381  			config: &CSVConfig{
   382  				Quote:     "'",
   383  				Delimiter: "'",
   384  			},
   385  			wantErr: "csv config quote and delimiter has common characters which is not allowed",
   386  		},
   387  		{
   388  			name: "delimiter and quote contain common characters",
   389  			config: &CSVConfig{
   390  				Quote:     "E",
   391  				Delimiter: "FE",
   392  			},
   393  			wantErr: "csv config quote and delimiter has common characters which is not allowed",
   394  		},
   395  		{
   396  			name: "invalid binary encoding method",
   397  			config: &CSVConfig{
   398  				Quote:                "\"",
   399  				Delimiter:            ",",
   400  				BinaryEncodingMethod: "invalid",
   401  			},
   402  			wantErr: "csv config binary-encoding-method can only be hex or base64",
   403  		},
   404  	}
   405  	for _, c := range tests {
   406  		tc := c
   407  		t.Run(tc.name, func(t *testing.T) {
   408  			t.Parallel()
   409  			s := &SinkConfig{
   410  				CSVConfig: tc.config,
   411  			}
   412  			if tc.wantErr == "" {
   413  				require.Nil(t, s.CSVConfig.validateAndAdjust())
   414  			} else {
   415  				require.Regexp(t, tc.wantErr, s.CSVConfig.validateAndAdjust())
   416  			}
   417  		})
   418  	}
   419  }
   420  
   421  func TestValidateAndAdjustStorageConfig(t *testing.T) {
   422  	t.Parallel()
   423  
   424  	sinkURI, err := url.Parse("s3://bucket?protocol=csv")
   425  	require.NoError(t, err)
   426  	s := GetDefaultReplicaConfig()
   427  	err = s.ValidateAndAdjust(sinkURI)
   428  	require.NoError(t, err)
   429  	require.Equal(t, DefaultFileIndexWidth, util.GetOrZero(s.Sink.FileIndexWidth))
   430  
   431  	err = s.ValidateAndAdjust(sinkURI)
   432  	require.NoError(t, err)
   433  	require.Equal(t, DefaultFileIndexWidth, util.GetOrZero(s.Sink.FileIndexWidth))
   434  
   435  	s.Sink.FileIndexWidth = util.AddressOf(16)
   436  	err = s.ValidateAndAdjust(sinkURI)
   437  	require.NoError(t, err)
   438  	require.Equal(t, 16, util.GetOrZero(s.Sink.FileIndexWidth))
   439  }