@@ -433,16 +433,24 @@ def collect_copy_events(request, check):
433433 return list (iter_agent_rpc_stream_copy_events (request , StaticPostgresCheckRegistry ([check ])))
434434
435435
436+ def event_metadata (event ):
437+ return event .metadata
438+
439+
440+ def event_payload (event ):
441+ return event .payload
442+
443+
436444def test_copy_stream_requires_explicit_operation_before_pool_access ():
437445 pool = FakePool (copy_blocks = [b'1\n ' ])
438446 request = valid_copy_request ()
439447 request .pop ('operation' )
440448
441449 events = collect_copy_events (request , make_check (pool = pool ))
442450
443- assert events [- 1 ]['status' ] == 'FAILED'
444- assert events [- 1 ]['error' ]['code' ] == 'invalid_request'
445- assert 'operation' in events [- 1 ]['error' ]['message' ]
451+ assert event_metadata ( events [- 1 ]) ['status' ] == 'FAILED'
452+ assert event_metadata ( events [- 1 ]) ['error' ]['code' ] == 'invalid_request'
453+ assert 'operation' in event_metadata ( events [- 1 ]) ['error' ]['message' ]
446454 assert pool .requested_dbnames == []
447455
448456
@@ -452,9 +460,9 @@ def test_copy_stream_rejects_unknown_fields_without_echoing_secrets(caplog):
452460
453461 events = collect_copy_events (request , make_check (pool = pool ))
454462
455- assert events [- 1 ]['status' ] == 'FAILED'
456- assert events [- 1 ]['error' ]['code' ] == 'invalid_request'
457- assert 'password' in events [- 1 ]['error' ]['message' ]
463+ assert event_metadata ( events [- 1 ]) ['status' ] == 'FAILED'
464+ assert event_metadata ( events [- 1 ]) ['error' ]['code' ] == 'invalid_request'
465+ assert 'password' in event_metadata ( events [- 1 ]) ['error' ]['message' ]
458466 assert 'SECRET_DO_NOT_LOG' not in str (events )
459467 assert 'SECRET_DO_NOT_LOG' not in caplog .text
460468 assert pool .requested_dbnames == []
@@ -466,9 +474,9 @@ def test_copy_stream_rejects_non_copy_allowlisted_queries_before_pool_access():
466474
467475 events = collect_copy_events (request , make_check (pool = pool ))
468476
469- assert events [- 1 ]['status' ] == 'FAILED'
470- assert events [- 1 ]['error' ]['code' ] == 'invalid_request'
471- assert 'query' in events [- 1 ]['error' ]['message' ]
477+ assert event_metadata ( events [- 1 ]) ['status' ] == 'FAILED'
478+ assert event_metadata ( events [- 1 ]) ['error' ]['code' ] == 'invalid_request'
479+ assert 'query' in event_metadata ( events [- 1 ]) ['error' ]['message' ]
472480 assert pool .requested_dbnames == []
473481
474482
@@ -478,33 +486,46 @@ def test_copy_stream_uses_connection_pool_and_emits_chunked_copy_bytes():
478486
479487 events = collect_copy_events (valid_copy_request (), check )
480488
481- assert events [0 ]['type' ] == 'metadata'
482- assert events [0 ]['operation' ] == 'copy_stream'
483- assert events [0 ]['format' ] == 'csv'
484- data_events = [event for event in events if event ['type' ] == 'data' ]
485- assert [event ['sequence' ] for event in data_events ] == [0 , 1 , 2 ]
486- assert [event ['data' ] for event in data_events ] == [b'abcdefgh' , b'ijklmnop' , b'qr' ]
487- assert [event ['bytes' ] for event in data_events ] == [8 , 8 , 2 ]
488- assert events [- 1 ]['type' ] == 'final'
489- assert events [- 1 ]['status' ] == 'SUCCEEDED'
490- assert events [- 1 ]['stats' ]['bytesEmitted' ] == 18
491- assert events [- 1 ]['stats' ]['chunksEmitted' ] == 3
489+ assert events [0 ].event_type == 'metadata'
490+ assert event_metadata (events [0 ])['operation' ] == 'copy_stream'
491+ assert event_metadata (events [0 ])['format' ] == 'csv'
492+ data_events = [event for event in events if event .event_type == 'data' ]
493+ assert [event_metadata (event )['sequence' ] for event in data_events ] == [0 , 1 , 2 ]
494+ assert [event_metadata (event )['offset' ] for event in data_events ] == [0 , 8 , 16 ]
495+ assert [event_payload (event ) for event in data_events ] == [b'abcdefgh' , b'ijklmnop' , b'qr' ]
496+ assert [event_metadata (event )['bytes' ] for event in data_events ] == [8 , 8 , 2 ]
497+ assert events [- 1 ].event_type == 'final'
498+ assert event_metadata (events [- 1 ])['status' ] == 'SUCCEEDED'
499+ assert event_metadata (events [- 1 ])['stats' ]['bytesEmitted' ] == 18
500+ assert event_metadata (events [- 1 ])['stats' ]['chunksEmitted' ] == 3
492501 assert pool .requested_dbnames == ['datadog_test' ]
493502 assert pool .closed_copies == 1
494503
495504
505+ def test_copy_stream_data_event_payload_preserves_arbitrary_bytes ():
506+ arbitrary_bytes = b'\x00 \xff \x80 abc\n '
507+ pool = FakePool (copy_blocks = [arbitrary_bytes ])
508+
509+ events = collect_copy_events (valid_copy_request (), make_check (pool = pool ))
510+
511+ data_events = [event for event in events if event .event_type == 'data' ]
512+ assert len (data_events ) == 1
513+ assert event_payload (data_events [0 ]) == arbitrary_bytes
514+ assert isinstance (event_payload (data_events [0 ]), bytes )
515+
516+
496517def test_copy_stream_enforces_max_bytes_without_exceeding_limit ():
497518 pool = FakePool (copy_blocks = [b'abcdefgh' , b'ijklmnop' ])
498519 request = valid_copy_request (limits = {'chunkBytes' : 8 , 'maxBytes' : 10 , 'maxRowBytes' : 32 , 'timeoutMs' : 5000 })
499520
500521 events = collect_copy_events (request , make_check (pool = pool ))
501522
502- data_events = [event for event in events if event [ 'type' ] == 'data' ]
503- assert [event [ 'data' ] for event in data_events ] == [b'abcdefgh' , b'ij' ]
504- assert sum (event ['bytes' ] for event in data_events ) == 10
505- assert events [- 1 ]['status' ] == 'FAILED'
506- assert events [- 1 ]['error' ]['code' ] == 'max_bytes_exceeded'
507- assert events [- 1 ]['stats' ]['bytesEmitted' ] == 10
523+ data_events = [event for event in events if event . event_type == 'data' ]
524+ assert [event_payload ( event ) for event in data_events ] == [b'abcdefgh' , b'ij' ]
525+ assert sum (event_metadata ( event ) ['bytes' ] for event in data_events ) == 10
526+ assert event_metadata ( events [- 1 ]) ['status' ] == 'FAILED'
527+ assert event_metadata ( events [- 1 ]) ['error' ]['code' ] == 'max_bytes_exceeded'
528+ assert event_metadata ( events [- 1 ]) ['stats' ]['bytesEmitted' ] == 10
508529 assert pool .closed_copies == 1
509530
510531
@@ -513,32 +534,41 @@ def test_copy_stream_enforces_max_row_bytes_after_copy_block_arrives():
513534
514535 events = collect_copy_events (valid_copy_request (), make_check (pool = pool ))
515536
516- assert [event [ 'data' ] for event in events if event [ 'type' ] == 'data' ] == []
517- assert events [- 1 ]['status' ] == 'FAILED'
518- assert events [- 1 ]['error' ]['code' ] == 'max_row_bytes_exceeded'
519- assert 'row granularity' in events [- 1 ]['error' ]['message' ]
537+ assert [event_payload ( event ) for event in events if event . event_type == 'data' ] == []
538+ assert event_metadata ( events [- 1 ]) ['status' ] == 'FAILED'
539+ assert event_metadata ( events [- 1 ]) ['error' ]['code' ] == 'max_row_bytes_exceeded'
540+ assert 'row granularity' in event_metadata ( events [- 1 ]) ['error' ]['message' ]
520541 assert pool .closed_copies == 1
521542
522543
523- def test_agent_rpc_stream_copy_adapts_iterator_to_callback ():
524- pool = FakePool (copy_blocks = [b'1\n ' ])
544+ def test_agent_rpc_stream_copy_adapts_iterator_to_binary_safe_callback ():
545+ arbitrary_bytes = b'\x00 \xff \x80 abc\n '
546+ pool = FakePool (copy_blocks = [arbitrary_bytes ])
525547 events = []
526548
527- execute_agent_rpc_stream_copy (json .dumps (valid_copy_request ()), make_check (pool = pool ), events .append )
549+ execute_agent_rpc_stream_copy (
550+ json .dumps (valid_copy_request ()), make_check (pool = pool ), lambda * event : events .append (event )
551+ )
528552
529- assert [event ['type' ] for event in events ] == ['metadata' , 'data' , 'final' ]
530- assert events [1 ]['data' ] == b'1\n '
531- assert events [- 1 ]['status' ] == 'SUCCEEDED'
553+ assert [event [0 ] for event in events ] == ['metadata' , 'data' , 'final' ]
554+ assert json .loads (events [1 ][1 ])['bytes' ] == len (arbitrary_bytes )
555+ assert events [1 ][2 ] == arbitrary_bytes
556+ assert isinstance (events [1 ][2 ], bytes )
557+ assert json .loads (events [- 1 ][1 ])['status' ] == 'SUCCEEDED'
532558
533559
534560def test_agent_rpc_stream_copy_rejects_malformed_json_without_echoing_input (caplog ):
535561 pool = FakePool (copy_blocks = [b'1\n ' ])
536562 events = []
537563
538- execute_agent_rpc_stream_copy ('{"password": "SECRET_DO_NOT_LOG"' , make_check (pool = pool ), events .append )
564+ execute_agent_rpc_stream_copy (
565+ '{"password": "SECRET_DO_NOT_LOG"' , make_check (pool = pool ), lambda * event : events .append (event )
566+ )
539567
540- assert events [- 1 ]['status' ] == 'FAILED'
541- assert events [- 1 ]['error' ]['code' ] == 'invalid_request'
568+ metadata = json .loads (events [- 1 ][1 ])
569+ assert events [- 1 ][0 ] == 'error'
570+ assert metadata ['status' ] == 'FAILED'
571+ assert metadata ['error' ]['code' ] == 'invalid_request'
542572 assert 'SECRET_DO_NOT_LOG' not in str (events )
543573 assert 'SECRET_DO_NOT_LOG' not in caplog .text
544574 assert pool .requested_dbnames == []
@@ -548,13 +578,13 @@ def test_agent_rpc_stream_copy_closes_copy_when_callback_raises():
548578 pool = FakePool (copy_blocks = [b'12345678' , b'abcdef' ])
549579 events = []
550580
551- def emit (event ):
552- events .append (event )
553- if event [ 'type' ] == 'data' :
581+ def emit (event_type , metadata_json , payload ):
582+ events .append (( event_type , metadata_json , payload ) )
583+ if event_type == 'data' :
554584 raise RuntimeError ('stop streaming' )
555585
556586 with pytest .raises (RuntimeError , match = 'stop streaming' ):
557587 execute_agent_rpc_stream_copy (json .dumps (valid_copy_request ()), make_check (pool = pool ), emit )
558588
559- assert [event ['type' ] for event in events ] == ['metadata' , 'data' ]
589+ assert [event [0 ] for event in events ] == ['metadata' , 'data' ]
560590 assert pool .closed_copies == 1
0 commit comments