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 }