@@ -867,6 +867,230 @@ describe('POST /source_discover', () => {
867867 } )
868868} )
869869
870+ // ---------------------------------------------------------------------------
871+ // JSON body mode (Content-Type: application/json)
872+ // ---------------------------------------------------------------------------
873+
874+ const syncParamsObj = JSON . parse ( syncParams )
875+
876+ describe ( 'JSON body mode' , ( ) => {
877+ it ( 'POST /pipeline_check accepts pipeline in JSON body' , async ( ) => {
878+ const app = await createApp ( resolver )
879+ const res = await app . request ( '/pipeline_check' , {
880+ method : 'POST' ,
881+ headers : { 'Content-Type' : 'application/json' } ,
882+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
883+ } )
884+ expect ( res . status ) . toBe ( 200 )
885+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'application/x-ndjson' )
886+ const events = await readNdjson < Record < string , unknown > > ( res )
887+ const statuses = events . filter ( ( e ) => e . type === 'connection_status' )
888+ expect ( statuses ) . toHaveLength ( 2 )
889+ } )
890+
891+ it ( 'POST /pipeline_setup accepts pipeline in JSON body' , async ( ) => {
892+ const app = await createApp ( resolver )
893+ const res = await app . request ( '/pipeline_setup' , {
894+ method : 'POST' ,
895+ headers : { 'Content-Type' : 'application/json' } ,
896+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
897+ } )
898+ expect ( res . status ) . toBe ( 200 )
899+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'application/x-ndjson' )
900+ } )
901+
902+ it ( 'POST /pipeline_teardown accepts pipeline in JSON body' , async ( ) => {
903+ const app = await createApp ( resolver )
904+ const res = await app . request ( '/pipeline_teardown' , {
905+ method : 'POST' ,
906+ headers : { 'Content-Type' : 'application/json' } ,
907+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
908+ } )
909+ expect ( res . status ) . toBe ( 200 )
910+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'application/x-ndjson' )
911+ } )
912+
913+ it ( 'POST /source_discover accepts source in JSON body' , async ( ) => {
914+ const app = await createApp ( resolver )
915+ const res = await app . request ( '/source_discover' , {
916+ method : 'POST' ,
917+ headers : { 'Content-Type' : 'application/json' } ,
918+ body : JSON . stringify ( {
919+ source : { type : 'test' , test : { streams : { customers : { } } } } ,
920+ } ) ,
921+ } )
922+ expect ( res . status ) . toBe ( 200 )
923+ const events = await readNdjson < Record < string , unknown > > ( res )
924+ expect ( events . some ( ( e ) => e . type === 'catalog' ) ) . toBe ( true )
925+ } )
926+
927+ it ( 'POST /pipeline_read accepts pipeline + state + body array in JSON body' , async ( ) => {
928+ const app = await createApp ( resolver )
929+ const res = await app . request ( '/pipeline_read' , {
930+ method : 'POST' ,
931+ headers : { 'Content-Type' : 'application/json' } ,
932+ body : JSON . stringify ( {
933+ pipeline : syncParamsObj ,
934+ body : [
935+ {
936+ type : 'record' ,
937+ record : {
938+ stream : 'customers' ,
939+ data : { id : 'cus_1' , name : 'Alice' } ,
940+ emitted_at : new Date ( ) . toISOString ( ) ,
941+ } ,
942+ } ,
943+ {
944+ type : 'source_state' ,
945+ source_state : { stream : 'customers' , data : { status : 'complete' } } ,
946+ } ,
947+ ] ,
948+ } ) ,
949+ } )
950+ expect ( res . status ) . toBe ( 200 )
951+ const events = await readNdjson < Message > ( res )
952+ expect ( events ) . toHaveLength ( 3 )
953+ expect ( events [ 0 ] ! . type ) . toBe ( 'record' )
954+ expect ( events [ 1 ] ! . type ) . toBe ( 'source_state' )
955+ expect ( events [ 2 ] ) . toMatchObject ( { type : 'eof' , eof : { reason : 'complete' } } )
956+ } )
957+
958+ it ( 'POST /pipeline_read accepts pipeline in JSON body without input' , async ( ) => {
959+ const app = await createApp ( resolver )
960+ const res = await app . request ( '/pipeline_read' , {
961+ method : 'POST' ,
962+ headers : { 'Content-Type' : 'application/json' } ,
963+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
964+ } )
965+ expect ( res . status ) . toBe ( 200 )
966+ const events = await readNdjson < Message > ( res )
967+ expect ( events . some ( ( e ) => e . type === 'eof' ) ) . toBe ( true )
968+ } )
969+
970+ it ( 'POST /pipeline_write accepts pipeline + body array in JSON body' , async ( ) => {
971+ const app = await createApp ( resolver )
972+ const res = await app . request ( '/pipeline_write' , {
973+ method : 'POST' ,
974+ headers : { 'Content-Type' : 'application/json' } ,
975+ body : JSON . stringify ( {
976+ pipeline : syncParamsObj ,
977+ body : [
978+ {
979+ type : 'record' ,
980+ record : {
981+ stream : 'customers' ,
982+ data : { id : 'cus_1' } ,
983+ emitted_at : '2024-01-01T00:00:00.000Z' ,
984+ } ,
985+ } ,
986+ {
987+ type : 'source_state' ,
988+ source_state : { stream : 'customers' , data : { cursor : 'cus_1' } } ,
989+ } ,
990+ ] ,
991+ } ) ,
992+ } )
993+ expect ( res . status ) . toBe ( 200 )
994+ const events = await readNdjson < Record < string , unknown > > ( res )
995+ expect ( events . some ( ( e ) => e . type === 'source_state' ) ) . toBe ( true )
996+ } )
997+
998+ it ( 'POST /pipeline_sync accepts pipeline + state + body array in JSON body' , async ( ) => {
999+ const app = await createApp ( resolver )
1000+ const res = await app . request ( '/pipeline_sync' , {
1001+ method : 'POST' ,
1002+ headers : { 'Content-Type' : 'application/json' } ,
1003+ body : JSON . stringify ( {
1004+ pipeline : syncParamsObj ,
1005+ body : [
1006+ {
1007+ type : 'record' ,
1008+ record : {
1009+ stream : 'customers' ,
1010+ data : { id : 'cus_1' , name : 'Alice' } ,
1011+ emitted_at : new Date ( ) . toISOString ( ) ,
1012+ } ,
1013+ } ,
1014+ {
1015+ type : 'source_state' ,
1016+ source_state : { stream : 'customers' , data : { status : 'complete' } } ,
1017+ } ,
1018+ ] ,
1019+ } ) ,
1020+ } )
1021+ expect ( res . status ) . toBe ( 200 )
1022+ const events = await readNdjson < Record < string , unknown > > ( res )
1023+ expect ( events . some ( ( e ) => e . type === 'source_state' ) ) . toBe ( true )
1024+ expect ( events . some ( ( e ) => e . type === 'eof' ) ) . toBe ( true )
1025+ } )
1026+
1027+ it ( 'POST /pipeline_sync without body array runs backfill mode' , async ( ) => {
1028+ const app = await createApp ( resolver )
1029+ const res = await app . request ( '/pipeline_sync' , {
1030+ method : 'POST' ,
1031+ headers : { 'Content-Type' : 'application/json' } ,
1032+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
1033+ } )
1034+ expect ( res . status ) . toBe ( 200 )
1035+ const events = await readNdjson < Record < string , unknown > > ( res )
1036+ expect ( events . some ( ( e ) => e . type === 'eof' ) ) . toBe ( true )
1037+ } )
1038+
1039+ it ( 'returns 400 when JSON body is missing pipeline' , async ( ) => {
1040+ const app = await createApp ( resolver )
1041+ const res = await app . request ( '/pipeline_check' , {
1042+ method : 'POST' ,
1043+ headers : { 'Content-Type' : 'application/json' } ,
1044+ body : JSON . stringify ( { } ) ,
1045+ } )
1046+ expect ( res . status ) . toBe ( 400 )
1047+ } )
1048+
1049+ it ( 'NDJSON content-type uses header mode even with JSON-like body' , async ( ) => {
1050+ const app = await createApp ( resolver )
1051+ const res = await app . request ( '/pipeline_check' , {
1052+ method : 'POST' ,
1053+ headers : {
1054+ 'Content-Type' : 'application/x-ndjson' ,
1055+ 'X-Pipeline' : syncParams ,
1056+ } ,
1057+ } )
1058+ expect ( res . status ) . toBe ( 200 )
1059+ } )
1060+
1061+ it ( 'no content-type defaults to header mode' , async ( ) => {
1062+ const app = await createApp ( resolver )
1063+ const res = await app . request ( '/pipeline_check' , {
1064+ method : 'POST' ,
1065+ headers : { 'X-Pipeline' : syncParams } ,
1066+ } )
1067+ expect ( res . status ) . toBe ( 200 )
1068+ } )
1069+
1070+ it ( 'mixed-case Content-Type is accepted as JSON body mode' , async ( ) => {
1071+ const app = await createApp ( resolver )
1072+ const res = await app . request ( '/pipeline_check' , {
1073+ method : 'POST' ,
1074+ headers : { 'Content-Type' : 'Application/JSON; charset=utf-8' } ,
1075+ body : JSON . stringify ( { pipeline : syncParamsObj } ) ,
1076+ } )
1077+ expect ( res . status ) . toBe ( 200 )
1078+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'application/x-ndjson' )
1079+ } )
1080+
1081+ it ( 'application/json-seq falls back to header mode, not JSON body' , async ( ) => {
1082+ const app = await createApp ( resolver )
1083+ const res = await app . request ( '/pipeline_check' , {
1084+ method : 'POST' ,
1085+ headers : {
1086+ 'Content-Type' : 'application/json-seq' ,
1087+ 'X-Pipeline' : syncParams ,
1088+ } ,
1089+ } )
1090+ expect ( res . status ) . toBe ( 200 )
1091+ } )
1092+ } )
1093+
8701094// ---------------------------------------------------------------------------
8711095// POST /internal/query
8721096// ---------------------------------------------------------------------------
0 commit comments