55package io .modelcontextprotocol .client .transport ;
66
77import java .util .Optional ;
8+ import java .util .stream .Stream ;
89
910import org .junit .jupiter .api .Test ;
11+ import org .junit .jupiter .params .ParameterizedTest ;
12+ import org .junit .jupiter .params .provider .Arguments ;
13+ import org .junit .jupiter .params .provider .MethodSource ;
14+ import org .junit .jupiter .params .provider .ValueSource ;
1015
1116import io .modelcontextprotocol .client .transport .ResponseBodyHandlers .SseEvent ;
1217import io .modelcontextprotocol .client .transport .ResponseBodyHandlers .SseEventParser ;
18+ import io .modelcontextprotocol .spec .McpTransportException ;
1319
1420import static org .assertj .core .api .Assertions .assertThat ;
21+ import static org .assertj .core .api .Assertions .assertThatThrownBy ;
1522
1623class SseEventParserTests {
1724
@@ -27,13 +34,56 @@ void simpleDataEvent() {
2734 }
2835
2936 @ Test
30- void multiLineDataAccumulatesWithNewlineSeparatorAndTrims () {
37+ void multiLineDataAccumulatesWithNewlineSeparatorAndPreservesWhitespace () {
3138 SseEventParser p = new SseEventParser (Integer .MAX_VALUE );
32- assertThat (p .feed ("data: first" )).isEmpty ();
33- assertThat (p .feed ("data: second " )).isEmpty ();
39+ assertThat (p .feed ("data: first " )).isEmpty ();
40+ assertThat (p .feed ("data: \t second \t " )).isEmpty ();
3441 Optional <SseEvent > event = p .feed ("" );
3542 assertThat (event ).isPresent ();
36- assertThat (event .get ().data ()).isEqualTo ("first\n second" );
43+ assertThat (event .get ().data ()).isEqualTo (" first \n \t second\t " );
44+ }
45+
46+ @ ParameterizedTest
47+ @ MethodSource ("fieldValues" )
48+ void fieldValuesRemoveAtMostOneLeadingSpace (String input , String expected ) {
49+ SseEventParser p = new SseEventParser (Integer .MAX_VALUE );
50+ p .feed ("id:" + input );
51+ p .feed ("event:" + input );
52+ p .feed ("data:" + input );
53+ SseEvent first = p .feed ("" ).orElseThrow ();
54+ assertThat (first .id ()).isEqualTo (expected );
55+ assertThat (first .event ()).isEqualTo (expected );
56+ assertThat (first .data ()).isEqualTo (expected );
57+
58+ p .feed ("data: next" );
59+ SseEvent second = p .feed ("" ).orElseThrow ();
60+ assertThat (second .id ()).isEqualTo (expected );
61+ assertThat (second .event ()).isEqualTo ("message" );
62+ assertThat (second .data ()).isEqualTo ("next" );
63+ }
64+
65+ static Stream <Arguments > fieldValues () {
66+ return Stream .of (Arguments .of ("token" , "token" ), Arguments .of (" token" , "token" ),
67+ Arguments .of (" token " , "token " ), Arguments .of (" token " , " token " ),
68+ Arguments .of ("\t token\t " , "\t token\t " ), Arguments .of (" \t token\t " , "\t token\t " ),
69+ Arguments .of (" " , " " ), Arguments .of ("\t " , "\t " ));
70+ }
71+
72+ @ Test
73+ void emptyDataLinesPreserveLeadingAndTrailingNewlines () {
74+ SseEventParser p = new SseEventParser (Integer .MAX_VALUE );
75+ p .feed ("data:" );
76+ p .feed ("data: payload" );
77+ p .feed ("data:" );
78+ assertThat (p .feed ("" ).orElseThrow ().data ()).isEqualTo ("\n payload\n " );
79+ }
80+
81+ @ Test
82+ void preservedWhitespaceCountsTowardsSizeLimit () {
83+ SseEventParser p = new SseEventParser (3 );
84+ p .feed ("data: a " );
85+ assertThat (p .feed ("" ).orElseThrow ().data ()).isEqualTo (" a " );
86+ assertThatThrownBy (() -> p .feed ("data: a " )).isInstanceOf (McpTransportException .class );
3787 }
3888
3989 @ Test
@@ -90,11 +140,12 @@ void emptyIdClearsLastEventId() {
90140 assertThat (p .feed ("" ).orElseThrow ().id ()).isNull ();
91141 }
92142
93- @ Test
94- void idContainingNullIsIgnored () {
143+ @ ParameterizedTest
144+ @ ValueSource (strings = { "\0 token" , "token\0 " , "to\0 ken" })
145+ void idContainingNullIsIgnored (String id ) {
95146 SseEventParser p = new SseEventParser (Integer .MAX_VALUE );
96147 p .feed ("id: 1" );
97- p .feed ("id: 2 \u0000 " + "3" );
148+ p .feed ("id: " + id );
98149 p .feed ("data: payload" );
99150 assertThat (p .feed ("" ).orElseThrow ().id ()).isEqualTo ("1" );
100151 }
0 commit comments