github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/changefeedccl/encoder_test.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Licensed as a CockroachDB Enterprise file under the Cockroach Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt 8 9 package changefeedccl 10 11 import ( 12 "context" 13 gosql "database/sql" 14 "encoding/binary" 15 gojson "encoding/json" 16 "fmt" 17 "net/http" 18 "net/http/httptest" 19 "testing" 20 21 "github.com/cockroachdb/cockroach-go/crdb" 22 "github.com/cockroachdb/cockroach/pkg/ccl/changefeedccl/cdctest" 23 "github.com/cockroachdb/cockroach/pkg/ccl/changefeedccl/changefeedbase" 24 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 25 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 26 "github.com/cockroachdb/cockroach/pkg/testutils" 27 "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" 28 "github.com/cockroachdb/cockroach/pkg/util/hlc" 29 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 30 "github.com/cockroachdb/cockroach/pkg/util/syncutil" 31 "github.com/cockroachdb/cockroach/pkg/workload/ledger" 32 "github.com/cockroachdb/cockroach/pkg/workload/workloadsql" 33 "github.com/cockroachdb/errors" 34 "github.com/linkedin/goavro" 35 "github.com/stretchr/testify/require" 36 ) 37 38 func TestEncoders(t *testing.T) { 39 defer leaktest.AfterTest(t)() 40 41 tableDesc, err := parseTableDesc(`CREATE TABLE foo (a INT PRIMARY KEY, b STRING)`) 42 require.NoError(t, err) 43 row := sqlbase.EncDatumRow{ 44 sqlbase.EncDatum{Datum: tree.NewDInt(1)}, 45 sqlbase.EncDatum{Datum: tree.NewDString(`bar`)}, 46 } 47 ts := hlc.Timestamp{WallTime: 1, Logical: 2} 48 49 var opts []map[string]string 50 for _, f := range []string{string(changefeedbase.OptFormatJSON), string(changefeedbase.OptFormatAvro)} { 51 for _, e := range []string{ 52 string(changefeedbase.OptEnvelopeKeyOnly), string(changefeedbase.OptEnvelopeRow), string(changefeedbase.OptEnvelopeWrapped), 53 } { 54 opts = append(opts, 55 map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e}, 56 map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptDiff: ``}, 57 map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptUpdatedTimestamps: ``}, 58 map[string]string{changefeedbase.OptFormat: f, changefeedbase.OptEnvelope: e, changefeedbase.OptUpdatedTimestamps: ``, changefeedbase.OptDiff: ``}, 59 ) 60 } 61 } 62 63 expecteds := map[string]struct { 64 // Either err is set or all of insert, delete, and resolved are. 65 err string 66 insert string 67 delete string 68 resolved string 69 }{ 70 `format=json,envelope=key_only`: { 71 insert: `[1]->`, 72 delete: `[1]->`, 73 resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`, 74 }, 75 `format=json,envelope=key_only,updated`: { 76 insert: `[1]->`, 77 delete: `[1]->`, 78 resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`, 79 }, 80 `format=json,envelope=key_only,diff`: { 81 err: `diff is only usable with envelope=wrapped`, 82 }, 83 `format=json,envelope=key_only,updated,diff`: { 84 err: `diff is only usable with envelope=wrapped`, 85 }, 86 `format=json,envelope=row`: { 87 insert: `[1]->{"a": 1, "b": "bar"}`, 88 delete: `[1]->`, 89 resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`, 90 }, 91 `format=json,envelope=row,updated`: { 92 insert: `[1]->{"__crdb__": {"updated": "1.0000000002"}, "a": 1, "b": "bar"}`, 93 delete: `[1]->`, 94 resolved: `{"__crdb__":{"resolved":"1.0000000002"}}`, 95 }, 96 `format=json,envelope=row,diff`: { 97 err: `diff is only usable with envelope=wrapped`, 98 }, 99 `format=json,envelope=row,updated,diff`: { 100 err: `diff is only usable with envelope=wrapped`, 101 }, 102 `format=json,envelope=wrapped`: { 103 insert: `[1]->{"after": {"a": 1, "b": "bar"}}`, 104 delete: `[1]->{"after": null}`, 105 resolved: `{"resolved":"1.0000000002"}`, 106 }, 107 `format=json,envelope=wrapped,updated`: { 108 insert: `[1]->{"after": {"a": 1, "b": "bar"}, "updated": "1.0000000002"}`, 109 delete: `[1]->{"after": null, "updated": "1.0000000002"}`, 110 resolved: `{"resolved":"1.0000000002"}`, 111 }, 112 `format=json,envelope=wrapped,diff`: { 113 insert: `[1]->{"after": {"a": 1, "b": "bar"}, "before": null}`, 114 delete: `[1]->{"after": null, "before": {"a": 1, "b": "bar"}}`, 115 resolved: `{"resolved":"1.0000000002"}`, 116 }, 117 `format=json,envelope=wrapped,updated,diff`: { 118 insert: `[1]->{"after": {"a": 1, "b": "bar"}, "before": null, "updated": "1.0000000002"}`, 119 delete: `[1]->{"after": null, "before": {"a": 1, "b": "bar"}, "updated": "1.0000000002"}`, 120 resolved: `{"resolved":"1.0000000002"}`, 121 }, 122 `format=experimental_avro,envelope=key_only`: { 123 insert: `{"a":{"long":1}}->`, 124 delete: `{"a":{"long":1}}->`, 125 resolved: `{"resolved":{"string":"1.0000000002"}}`, 126 }, 127 `format=experimental_avro,envelope=key_only,updated`: { 128 err: `updated is only usable with envelope=wrapped`, 129 }, 130 `format=experimental_avro,envelope=key_only,diff`: { 131 err: `diff is only usable with envelope=wrapped`, 132 }, 133 `format=experimental_avro,envelope=key_only,updated,diff`: { 134 err: `updated is only usable with envelope=wrapped`, 135 }, 136 `format=experimental_avro,envelope=row`: { 137 err: `envelope=row is not supported with format=experimental_avro`, 138 }, 139 `format=experimental_avro,envelope=row,updated`: { 140 err: `envelope=row is not supported with format=experimental_avro`, 141 }, 142 `format=experimental_avro,envelope=row,diff`: { 143 err: `envelope=row is not supported with format=experimental_avro`, 144 }, 145 `format=experimental_avro,envelope=row,updated,diff`: { 146 err: `envelope=row is not supported with format=experimental_avro`, 147 }, 148 `format=experimental_avro,envelope=wrapped`: { 149 insert: `{"a":{"long":1}}->` + 150 `{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}}}`, 151 delete: `{"a":{"long":1}}->{"after":null}`, 152 resolved: `{"resolved":{"string":"1.0000000002"}}`, 153 }, 154 `format=experimental_avro,envelope=wrapped,updated`: { 155 insert: `{"a":{"long":1}}->` + 156 `{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` + 157 `"updated":{"string":"1.0000000002"}}`, 158 delete: `{"a":{"long":1}}->{"after":null,"updated":{"string":"1.0000000002"}}`, 159 resolved: `{"resolved":{"string":"1.0000000002"}}`, 160 }, 161 `format=experimental_avro,envelope=wrapped,diff`: { 162 insert: `{"a":{"long":1}}->` + 163 `{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` + 164 `"before":null}`, 165 delete: `{"a":{"long":1}}->` + 166 `{"after":null,` + 167 `"before":{"foo_before":{"a":{"long":1},"b":{"string":"bar"}}}}`, 168 resolved: `{"resolved":{"string":"1.0000000002"}}`, 169 }, 170 `format=experimental_avro,envelope=wrapped,updated,diff`: { 171 insert: `{"a":{"long":1}}->` + 172 `{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},` + 173 `"before":null,` + 174 `"updated":{"string":"1.0000000002"}}`, 175 delete: `{"a":{"long":1}}->` + 176 `{"after":null,` + 177 `"before":{"foo_before":{"a":{"long":1},"b":{"string":"bar"}}},` + 178 `"updated":{"string":"1.0000000002"}}`, 179 resolved: `{"resolved":{"string":"1.0000000002"}}`, 180 }, 181 } 182 183 for _, o := range opts { 184 name := fmt.Sprintf("format=%s,envelope=%s", o[changefeedbase.OptFormat], o[changefeedbase.OptEnvelope]) 185 if _, ok := o[changefeedbase.OptUpdatedTimestamps]; ok { 186 name += `,updated` 187 } 188 if _, ok := o[changefeedbase.OptDiff]; ok { 189 name += `,diff` 190 } 191 t.Run(name, func(t *testing.T) { 192 expected := expecteds[name] 193 194 var rowStringFn func([]byte, []byte) string 195 var resolvedStringFn func([]byte) string 196 switch o[changefeedbase.OptFormat] { 197 case string(changefeedbase.OptFormatJSON): 198 rowStringFn = func(k, v []byte) string { return fmt.Sprintf(`%s->%s`, k, v) } 199 resolvedStringFn = func(r []byte) string { return string(r) } 200 case string(changefeedbase.OptFormatAvro): 201 reg := makeTestSchemaRegistry() 202 defer reg.Close() 203 o[changefeedbase.OptConfluentSchemaRegistry] = reg.server.URL 204 rowStringFn = func(k, v []byte) string { 205 key, value := avroToJSON(t, reg, k), avroToJSON(t, reg, v) 206 return fmt.Sprintf(`%s->%s`, key, value) 207 } 208 resolvedStringFn = func(r []byte) string { 209 return string(avroToJSON(t, reg, r)) 210 } 211 default: 212 t.Fatalf(`unknown format: %s`, o[changefeedbase.OptFormat]) 213 } 214 215 e, err := getEncoder(o) 216 if len(expected.err) > 0 { 217 require.EqualError(t, err, expected.err) 218 return 219 } 220 require.NoError(t, err) 221 222 rowInsert := encodeRow{ 223 datums: row, 224 updated: ts, 225 tableDesc: tableDesc, 226 prevDatums: nil, 227 prevTableDesc: tableDesc, 228 } 229 keyInsert, err := e.EncodeKey(context.Background(), rowInsert) 230 require.NoError(t, err) 231 keyInsert = append([]byte(nil), keyInsert...) 232 valueInsert, err := e.EncodeValue(context.Background(), rowInsert) 233 require.NoError(t, err) 234 require.Equal(t, expected.insert, rowStringFn(keyInsert, valueInsert)) 235 236 rowDelete := encodeRow{ 237 datums: row, 238 deleted: true, 239 prevDatums: row, 240 updated: ts, 241 tableDesc: tableDesc, 242 prevTableDesc: tableDesc, 243 } 244 keyDelete, err := e.EncodeKey(context.Background(), rowDelete) 245 require.NoError(t, err) 246 keyDelete = append([]byte(nil), keyDelete...) 247 valueDelete, err := e.EncodeValue(context.Background(), rowDelete) 248 require.NoError(t, err) 249 require.Equal(t, expected.delete, rowStringFn(keyDelete, valueDelete)) 250 251 resolved, err := e.EncodeResolvedTimestamp(context.Background(), tableDesc.Name, ts) 252 require.NoError(t, err) 253 require.Equal(t, expected.resolved, resolvedStringFn(resolved)) 254 }) 255 } 256 } 257 258 type testSchemaRegistry struct { 259 server *httptest.Server 260 mu struct { 261 syncutil.Mutex 262 idAlloc int32 263 schemas map[int32]string 264 } 265 } 266 267 func makeTestSchemaRegistry() *testSchemaRegistry { 268 r := &testSchemaRegistry{} 269 r.mu.schemas = make(map[int32]string) 270 r.server = httptest.NewServer(http.HandlerFunc(r.Register)) 271 return r 272 } 273 274 func (r *testSchemaRegistry) Close() { 275 r.server.Close() 276 } 277 278 func (r *testSchemaRegistry) Register(hw http.ResponseWriter, hr *http.Request) { 279 type confluentSchemaVersionRequest struct { 280 Schema string `json:"schema"` 281 } 282 type confluentSchemaVersionResponse struct { 283 ID int32 `json:"id"` 284 } 285 if err := func() error { 286 defer hr.Body.Close() 287 var req confluentSchemaVersionRequest 288 if err := gojson.NewDecoder(hr.Body).Decode(&req); err != nil { 289 return err 290 } 291 292 r.mu.Lock() 293 id := r.mu.idAlloc 294 r.mu.idAlloc++ 295 r.mu.schemas[id] = req.Schema 296 r.mu.Unlock() 297 298 res, err := gojson.Marshal(confluentSchemaVersionResponse{ID: id}) 299 if err != nil { 300 return err 301 } 302 303 hw.Header().Set(`Content-type`, `application/json`) 304 _, _ = hw.Write(res) 305 return nil 306 }(); err != nil { 307 http.Error(hw, err.Error(), http.StatusInternalServerError) 308 } 309 } 310 311 func (r *testSchemaRegistry) encodedAvroToNative(b []byte) (interface{}, error) { 312 if len(b) == 0 || b[0] != confluentAvroWireFormatMagic { 313 return ``, errors.Errorf(`bad magic byte`) 314 } 315 b = b[1:] 316 if len(b) < 4 { 317 return ``, errors.Errorf(`missing registry id`) 318 } 319 id := int32(binary.BigEndian.Uint32(b[:4])) 320 b = b[4:] 321 322 r.mu.Lock() 323 jsonSchema := r.mu.schemas[id] 324 r.mu.Unlock() 325 codec, err := goavro.NewCodec(jsonSchema) 326 if err != nil { 327 return ``, err 328 } 329 native, _, err := codec.NativeFromBinary(b) 330 return native, err 331 } 332 333 func TestAvroEncoder(t *testing.T) { 334 defer leaktest.AfterTest(t)() 335 336 testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) { 337 ctx := context.Background() 338 reg := makeTestSchemaRegistry() 339 defer reg.Close() 340 341 sqlDB := sqlutils.MakeSQLRunner(db) 342 sqlDB.Exec(t, `CREATE TABLE foo (a INT PRIMARY KEY, b STRING)`) 343 var ts1 string 344 sqlDB.QueryRow(t, 345 `INSERT INTO foo VALUES (1, 'bar'), (2, NULL) RETURNING cluster_logical_timestamp()`, 346 ).Scan(&ts1) 347 348 foo := feed(t, f, `CREATE CHANGEFEED FOR foo `+ 349 `WITH format=$1, confluent_schema_registry=$2, diff, resolved`, 350 changefeedbase.OptFormatAvro, reg.server.URL) 351 defer closeFeed(t, foo) 352 assertPayloadsAvro(t, reg, foo, []string{ 353 `foo: {"a":{"long":1}}->{"after":{"foo":{"a":{"long":1},"b":{"string":"bar"}}},"before":null}`, 354 `foo: {"a":{"long":2}}->{"after":{"foo":{"a":{"long":2},"b":null}},"before":null}`, 355 }) 356 resolved := expectResolvedTimestampAvro(t, reg, foo) 357 if ts := parseTimeToHLC(t, ts1); resolved.LessEq(ts) { 358 t.Fatalf(`expected a resolved timestamp greater than %s got %s`, ts, resolved) 359 } 360 361 fooUpdated := feed(t, f, `CREATE CHANGEFEED FOR foo `+ 362 `WITH format=$1, confluent_schema_registry=$2, diff, updated`, 363 changefeedbase.OptFormatAvro, reg.server.URL) 364 defer closeFeed(t, fooUpdated) 365 // Skip over the first two rows since we don't know the statement timestamp. 366 _, err := fooUpdated.Next() 367 require.NoError(t, err) 368 _, err = fooUpdated.Next() 369 require.NoError(t, err) 370 371 var ts2 string 372 require.NoError(t, crdb.ExecuteTx(ctx, db, nil /* txopts */, func(tx *gosql.Tx) error { 373 return tx.QueryRow( 374 `INSERT INTO foo VALUES (3, 'baz') RETURNING cluster_logical_timestamp()`, 375 ).Scan(&ts2) 376 })) 377 assertPayloadsAvro(t, reg, fooUpdated, []string{ 378 `foo: {"a":{"long":3}}->{"after":{"foo":{"a":{"long":3},"b":{"string":"baz"}}},` + 379 `"before":null,` + 380 `"updated":{"string":"` + ts2 + `"}}`, 381 }) 382 } 383 384 t.Run(`sinkless`, sinklessTest(testFn)) 385 t.Run(`enterprise`, enterpriseTest(testFn)) 386 } 387 388 func TestAvroMigrateToUnsupportedColumn(t *testing.T) { 389 defer leaktest.AfterTest(t)() 390 391 testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) { 392 reg := makeTestSchemaRegistry() 393 defer reg.Close() 394 395 sqlDB := sqlutils.MakeSQLRunner(db) 396 sqlDB.Exec(t, `CREATE TABLE foo (a INT PRIMARY KEY)`) 397 sqlDB.Exec(t, `INSERT INTO foo VALUES (1)`) 398 399 foo := feed(t, f, `CREATE CHANGEFEED FOR foo `+ 400 `WITH format=$1, confluent_schema_registry=$2`, 401 changefeedbase.OptFormatAvro, reg.server.URL) 402 defer closeFeed(t, foo) 403 assertPayloadsAvro(t, reg, foo, []string{ 404 `foo: {"a":{"long":1}}->{"after":{"foo":{"a":{"long":1}}}}`, 405 }) 406 407 sqlDB.Exec(t, `ALTER TABLE foo ADD COLUMN b OID`) 408 sqlDB.Exec(t, `INSERT INTO foo VALUES (2, 3::OID)`) 409 if _, err := foo.Next(); !testutils.IsError(err, `type OID not yet supported with avro`) { 410 t.Fatalf(`expected "type OID not yet supported with avro" error got: %+v`, err) 411 } 412 } 413 414 t.Run(`sinkless`, sinklessTest(testFn)) 415 t.Run(`enterprise`, enterpriseTest(testFn)) 416 } 417 418 func TestAvroLedger(t *testing.T) { 419 defer leaktest.AfterTest(t)() 420 421 testFn := func(t *testing.T, db *gosql.DB, f cdctest.TestFeedFactory) { 422 reg := makeTestSchemaRegistry() 423 defer reg.Close() 424 425 ctx := context.Background() 426 gen := ledger.FromFlags(`--customers=1`) 427 var l workloadsql.InsertsDataLoader 428 _, err := workloadsql.Setup(ctx, db, gen, l) 429 require.NoError(t, err) 430 431 ledger := feed(t, f, `CREATE CHANGEFEED FOR customer, transaction, entry, session 432 WITH format=$1, confluent_schema_registry=$2 433 `, changefeedbase.OptFormatAvro, reg.server.URL) 434 defer closeFeed(t, ledger) 435 436 assertPayloadsAvro(t, reg, ledger, []string{ 437 `customer: {"id":{"long":0}}->{"after":{"customer":{"balance":{"bytes.decimal":"0"},"created":{"long.timestamp-micros":"2114-03-27T13:14:27.287114Z"},"credit_limit":null,"currency_code":{"string":"XVL"},"id":{"long":0},"identifier":{"string":"0"},"is_active":{"boolean":true},"is_system_customer":{"boolean":true},"name":null,"sequence_number":{"long":-1}}}}`, 438 `entry: {"id":{"long":1543039099823358511}}->{"after":{"entry":{"amount":{"bytes.decimal":"0"},"created_ts":{"long.timestamp-micros":"1990-12-09T23:47:23.811124Z"},"customer_id":{"long":0},"id":{"long":1543039099823358511},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`, 439 `entry: {"id":{"long":2244708090865615074}}->{"after":{"entry":{"amount":{"bytes.decimal":"1/50"},"created_ts":{"long.timestamp-micros":"2075-11-08T22:07:12.055686Z"},"customer_id":{"long":0},"id":{"long":2244708090865615074},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`, 440 `entry: {"id":{"long":3305628230121721621}}->{"after":{"entry":{"amount":{"bytes.decimal":"1/25"},"created_ts":{"long.timestamp-micros":"2185-01-30T21:38:15.06669Z"},"customer_id":{"long":0},"id":{"long":3305628230121721621},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`, 441 `entry: {"id":{"long":4151935814835861840}}->{"after":{"entry":{"amount":{"bytes.decimal":"3/50"},"created_ts":{"long.timestamp-micros":"1684-10-05T17:51:40.795101Z"},"customer_id":{"long":0},"id":{"long":4151935814835861840},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"44061/500"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`, 442 `entry: {"id":{"long":5577006791947779410}}->{"after":{"entry":{"amount":{"bytes.decimal":"0"},"created_ts":{"long.timestamp-micros":"2185-11-07T09:42:42.666146Z"},"customer_id":{"long":0},"id":{"long":5577006791947779410},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`, 443 `entry: {"id":{"long":6640668014774057861}}->{"after":{"entry":{"amount":{"bytes.decimal":"-1/50"},"created_ts":{"long.timestamp-micros":"1690-05-19T13:29:46.145044Z"},"customer_id":{"long":0},"id":{"long":6640668014774057861},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}}}`, 444 `entry: {"id":{"long":7414159922357799360}}->{"after":{"entry":{"amount":{"bytes.decimal":"-1/25"},"created_ts":{"long.timestamp-micros":"1706-02-05T02:38:08.15195Z"},"customer_id":{"long":0},"id":{"long":7414159922357799360},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`, 445 `entry: {"id":{"long":8475284246537043955}}->{"after":{"entry":{"amount":{"bytes.decimal":"-3/50"},"created_ts":{"long.timestamp-micros":"2048-07-21T10:02:40.114474Z"},"customer_id":{"long":0},"id":{"long":8475284246537043955},"money_type":{"string":"C"},"system_amount":{"bytes.decimal":"-88123/1000"},"transaction_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}}}`, 446 `session: {"session_id":{"string":"pLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6AWX40IV"}}->{"after":{"session":{"data":{"string":"SP3NcHciWvqZTa3N06RxRTZHWUsaD7HEdz1ThbXfQ7pYSQ4n378l2VQKGNbSuJE0fQbzONJAAwdCxmM9BIabKERsUhPNmMmdf3eSJyYtqwcFiUILzXv3fcNIrWO8sToFgoilA1U2WxNeW2gdgUVDsEWJ88aX8tLF"},"expiry_timestamp":{"long.timestamp-micros":"2052-05-14T04:02:49.264975Z"},"last_update":{"long.timestamp-micros":"2070-03-19T02:10:22.552438Z"},"session_id":{"string":"pLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6AWX40IV"}}}}`, 447 `transaction: {"external_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"}}->{"after":{"transaction":{"context":{"string":"BpLnfgDsc3WD9F3qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9uVbhV4vC6"},"created_ts":{"long.timestamp-micros":"2178-08-01T19:10:30.064819Z"},"external_id":{"string":"payment:a8c7f832-281a-39c5-8820-1fb960ff6465"},"response":{"bytes":"MDZSeFJUWkhXVXNhRDdIRWR6MVRoYlhmUTdwWVNRNG4zNzhsMlZRS0dOYlN1SkUwZlFiek9OSkFBd2RDeG1NOUJJYWJLRVJzVWhQTm1NbWRmM2VTSnlZdHF3Y0ZpVUlMelh2M2ZjTklyV084c1RvRmdvaWxBMVUyV3hOZVcyZ2RnVVZEc0VXSjg4YVg4dExGSjk1cVlVN1VyTjljdGVjd1p0NlM1empoRDF0WFJUbWtZS1FvTjAyRm1XblFTSzN3UkM2VUhLM0txQXR4alAzWm1EMmp0dDR6Z3I2TWVVam9BamNPMGF6TW10VTRZdHYxUDhPUG1tU05hOThkN3RzdGF4eTZuYWNuSkJTdUZwT2h5SVhFN1BKMURoVWtMWHFZWW5FTnVucWRzd3BUdzVVREdEUzM0bVNQWUs4dm11YjNYOXVYSXU3Rk5jSmpBUlFUM1JWaFZydDI0UDdpNnhDckw2RmM0R2N1SEMxNGthdW5BVFVQUkhqR211Vm14SHN5enpCYnlPb25xVlVTREsxVg=="},"reversed_by":null,"systimestamp":{"long.timestamp-micros":"2215-07-28T23:47:01.795499Z"},"tcomment":null,"transaction_type_reference":{"long":400},"username":{"string":"WX40IVUWSP3NcHciWvqZ"}}}}`, 448 `transaction: {"external_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"}}->{"after":{"transaction":{"context":{"string":"KSiOW5eQ8sklpgstrQZtAcrsGvPnYSXMOpFIpPzS8iI5N2gN7lD1rYjT"},"created_ts":{"long.timestamp-micros":"2062-07-27T13:21:35.213969Z"},"external_id":{"string":"payment:e3757ca7-d646-66ea-2b8d-6116831cbb05"},"response":{"bytes":"bWdkbHVWOFVvcWpRM1JBTTRTWjNzT0M4ZnlzZXN5NnRYeVZ5WTBnWkE1aVNJUjM4MFVPVWFwQTlPRmpuRWtiaHF6MlRZSlZIWUFtTHI5R0kyMlo3NFVmNjhDMFRRb2RDdWF0NmhmWmZSYmFlV1pJSFExMGJsSjVqQUd2VVRpWWJOWHZPcWowYlRUM24xNmNqQVNEN29qN2RPbVlVbTFua3AybnVvWTZGZlgzcVFHY09SbHZ2UHdHaHNDZWlZTmpvTVRoUXBFc0ZrSVpZVUxxNFFORzc1M25mamJYdENaUm4xSmVZV1hpUW1IWjJZMWIxb1lZbUtBS05aQjF1MGt1TU5ZbEFISW5hY1JoTkFzakd6bnBKSXZZdmZqWXk3MXV4OVI5SkRNQUMxRUtOSGFZVWNlekk4OHRHYmdwbWFGaXdIV09sUFQ5RUJVcHh6MHlCSnZGM1BKcW5jejVwMnpnVVhDcm9kZTV6UG5pNjJQV1dtMk5pSWVkSUxFaExLVVNHVWRNU1R5N1pmcjRyY2RJTw=="},"reversed_by":null,"systimestamp":{"long.timestamp-micros":"2229-01-11T00:56:37.706179Z"},"tcomment":null,"transaction_type_reference":{"long":400},"username":{"string":"XJXORIpfMGxOaIIFFFts"}}}}`, 449 }) 450 } 451 452 t.Run(`sinkless`, sinklessTest(testFn)) 453 t.Run(`enterprise`, enterpriseTest(testFn)) 454 }