1414 * limitations under the License.
1515 */
1616package io .qameta .allure .grpc ;
17+
1718import com .fasterxml .jackson .databind .JsonNode ;
1819import com .fasterxml .jackson .databind .ObjectMapper ;
1920import io .grpc .ManagedChannel ;
2425import io .qameta .allure .Allure ;
2526import io .qameta .allure .model .Attachment ;
2627import io .qameta .allure .model .StepResult ;
28+ import io .qameta .allure .model .TestResult ;
2729import io .qameta .allure .test .AllureResults ;
2830import org .grpcmock .GrpcMock ;
2931import org .grpcmock .junit5 .GrpcMockExtension ;
@@ -77,7 +79,7 @@ void configureMockServer() {
7779 .willReturn (Response .newBuilder ().setMessage (RESPONSE_MESSAGE ).build ()));
7880
7981 GrpcMock .stubFor (bidiStreamingMethod (TestServiceGrpc .getCalculateBidiStreamMethod ())
80- .willProxyTo (responseObserver -> new StreamObserver <>() {
82+ .willProxyTo (responseObserver -> new StreamObserver <Request >() {
8183 @ Override
8284 public void onNext (Request request ) {
8385 responseObserver .onNext (Response .newBuilder ().setMessage (RESPONSE_MESSAGE ).build ());
@@ -174,10 +176,10 @@ void shouldCreateAttachmentsForClientStreamingWithAsynchronousStub() {
174176 TestServiceGrpc .TestServiceStub asynchronousStub =
175177 TestServiceGrpc .newStub (managedChannel ).withInterceptors (new AllureGrpc ());
176178
177- final List <Response > receivedResponses = new ArrayList <>();
179+ final List <Response > receivedResponses = new ArrayList <Response >();
178180
179181 Allure .step ("async-root-client-stream" , () -> {
180- StreamObserver <Response > responseObserver = new StreamObserver <>() {
182+ StreamObserver <Response > responseObserver = new StreamObserver <Response >() {
181183 @ Override
182184 public void onNext (Response value ) {
183185 receivedResponses .add (value );
@@ -213,7 +215,7 @@ void shouldCreateAttachmentsForBidirectionalStreamingWithAsynchronousStub() {
213215 List <Response > receivedResponses = new ArrayList <>();
214216
215217 Allure .step ("async-root-bidi-stream" , () -> {
216- StreamObserver <Response > responseObserver = new StreamObserver <>() {
218+ StreamObserver <Response > responseObserver = new StreamObserver <Response >() {
217219 @ Override public void onNext (Response value ) { receivedResponses .add (value ); }
218220 @ Override public void onError (Throwable throwable ) { }
219221 @ Override public void onCompleted () { }
@@ -245,8 +247,7 @@ void unaryRequestBodyIsCapturedAsJsonObject() throws Exception {
245247 assertThat (response .getMessage ()).isEqualTo ("ok" );
246248 });
247249
248- String attachmentHtmlContent = readAttachmentContentByName (allureResults , "gRPC request" );
249- String jsonPayload = extractJsonPayload (attachmentHtmlContent );
250+ String jsonPayload = readJsonAttachmentByName (allureResults , "gRPC request (json)" );
250251 JsonNode actualJsonNode = JSON .readTree (jsonPayload );
251252 JsonNode expectedJsonNode = JSON .createObjectNode ().put ("topic" , "topic-1" );
252253
@@ -267,8 +268,7 @@ void unaryResponseBodyIsCapturedAsJsonObject() throws Exception {
267268 assertThat (response .getMessage ()).isEqualTo ("hello-world" );
268269 });
269270
270- String attachmentHtmlContent = readAttachmentContentByName (allureResults , "gRPC response" );
271- String jsonPayload = extractJsonPayload (attachmentHtmlContent );
271+ String jsonPayload = readJsonAttachmentByName (allureResults , "gRPC response (json)" );
272272 JsonNode actualJsonNode = JSON .readTree (jsonPayload );
273273 JsonNode expectedJsonNode = JSON .createObjectNode ().put ("message" , "hello-world" );
274274
@@ -296,18 +296,33 @@ void serverStreamingResponseBodyIsJsonArrayInOrder() throws Exception {
296296 assertThat (responseIterator .hasNext ()).isFalse ();
297297 });
298298
299- String attachmentHtmlContent = readAttachmentContentByName (
300- allureResults ,
301- "gRPC response (collection of elements from Server stream)"
299+ String jsonPayload = readJsonAttachmentByName (
300+ allureResults , "gRPC response (collection of elements from Server stream) (json)"
302301 );
303- String jsonPayload = extractJsonPayload (attachmentHtmlContent );
304302 JsonNode actualJsonArray = JSON .readTree (jsonPayload );
305303
306304 assertThat (actualJsonArray .isArray ()).isTrue ();
307305 assertThat (actualJsonArray .size ()).isEqualTo (2 );
308306 assertThat (actualJsonArray .get (0 )).isEqualTo (JSON .createObjectNode ().put ("message" , "first" ));
309307 assertThat (actualJsonArray .get (1 )).isEqualTo (JSON .createObjectNode ().put ("message" , "second" ));
310308 }
309+ private static String readJsonAttachmentByName (AllureResults allureResults , String jsonAttachmentName ) {
310+ TestResult test = allureResults .getTestResults ().get (0 );
311+
312+ Attachment matchedAttachment = flattenSteps (test .getSteps ()).stream ()
313+ .flatMap (step -> step .getAttachments ().stream ())
314+ .filter (attachment -> jsonAttachmentName .equals (attachment .getName ()))
315+ .findFirst ()
316+ .orElseThrow (() -> new IllegalStateException ("Attachment not found: " + jsonAttachmentName ));
317+
318+ String attachmentSourceKey = matchedAttachment .getSource ();
319+ Map <String , byte []> attachmentsContent = allureResults .getAttachments ();
320+ byte [] rawAttachmentContent = attachmentsContent .get (attachmentSourceKey );
321+ if (rawAttachmentContent == null ) {
322+ throw new IllegalStateException ("Attachment content not found by source: " + attachmentSourceKey );
323+ }
324+ return new String (rawAttachmentContent , StandardCharsets .UTF_8 );
325+ }
311326
312327 protected final AllureResults executeUnary (Request request ) {
313328 return runWithinTestContext (() -> {
@@ -352,111 +367,6 @@ protected final AllureResults executeUnaryExpectingException(Request request) {
352367 );
353368 }
354369
355- private static String readAttachmentContentByName (AllureResults allureResults , String attachmentName ) {
356- var test = allureResults .getTestResults ().get (0 );
357-
358- Attachment matchedAttachment = flattenSteps (test .getSteps ()).stream ()
359- .flatMap (step -> step .getAttachments ().stream ())
360- .filter (attachment -> attachmentName .equals (attachment .getName ()))
361- .findFirst ()
362- .orElseThrow (() -> new IllegalStateException ("Attachment not found: " + attachmentName ));
363-
364- String attachmentSourceKey = matchedAttachment .getSource ();
365- Map <String , byte []> attachmentsContent = allureResults .getAttachments ();
366- byte [] rawAttachmentContent = attachmentsContent .get (attachmentSourceKey );
367- if (rawAttachmentContent == null ) {
368- throw new IllegalStateException ("Attachment content not found by source: " + attachmentSourceKey );
369- }
370- return new String (rawAttachmentContent , StandardCharsets .UTF_8 );
371- }
372-
373- private static String extractJsonPayload (String htmlContent ) {
374- String textWithoutHtml = stripHtmlTags (unescapeHtml (htmlContent ));
375- int fullLength = textWithoutHtml .length ();
376- for (int currentIndex = 0 ; currentIndex < fullLength ; currentIndex ++) {
377- char currentChar = textWithoutHtml .charAt (currentIndex );
378- if (currentChar == '{' || currentChar == '[' ) {
379- int matchingBracketIndex = findMatchingBracket (textWithoutHtml , currentIndex );
380- if (matchingBracketIndex > currentIndex ) {
381- String candidateJson = textWithoutHtml .substring (currentIndex , matchingBracketIndex + 1 ).trim ();
382- if (looksLikeJson (candidateJson ) && canParseJson (candidateJson )) {
383- return candidateJson ;
384- }
385- }
386- }
387- }
388- throw new IllegalStateException ("JSON payload not found or not valid inside attachment" );
389- }
390-
391- private static boolean canParseJson (String candidateJson ) {
392- try {
393- JSON .readTree (candidateJson );
394- return true ;
395- } catch (Exception ignore ) {
396- return false ;
397- }
398- }
399-
400- private static boolean looksLikeJson (String input ) {
401- if (input == null ) {
402- return false ;
403- }
404- String trimmed = input .trim ();
405- if (!(trimmed .startsWith ("{" ) || trimmed .startsWith ("[" ))) {
406- return false ;
407- }
408- return trimmed .matches ("(?s).*\" [^\" ]+\" \\ s*:\\ s*.*" );
409- }
410-
411- private static int findMatchingBracket (String input , int startIndex ) {
412- char openingBracket = input .charAt (startIndex );
413- char closingBracket = (openingBracket == '{' ) ? '}' : ']' ;
414- int nestingDepth = 0 ;
415- boolean insideString = false ;
416- for (int index = startIndex ; index < input .length (); index ++) {
417- char symbol = input .charAt (index );
418- if (symbol == '"' && (index == 0 || input .charAt (index - 1 ) != '\\' )) {
419- insideString = !insideString ;
420- }
421- if (insideString ) {
422- continue ;
423- }
424- if (symbol == openingBracket ) {
425- nestingDepth ++;
426- } else if (symbol == closingBracket ) {
427- nestingDepth --;
428- if (nestingDepth == 0 ) {
429- return index ;
430- }
431- }
432- }
433- return -1 ;
434- }
435-
436- private static String stripHtmlTags (String input ) {
437- String withoutTags = input .replaceAll ("(?is)<script.*?</script>" , "" )
438- .replaceAll ("(?is)<style.*?</style>" , "" )
439- .replaceAll ("(?s)<[^>]*>" , " " );
440- return withoutTags
441- .replace ("\r " , " " )
442- .replace ("\n " , " " )
443- .replaceAll ("[ \\ t\\ x0B\\ f\\ r]+" , " " )
444- .trim ();
445- }
446-
447- private static String unescapeHtml (String input ) {
448- return input .replace (""" , "\" " )
449- .replace ("<" , "<" )
450- .replace (">" , ">" )
451- .replace ("&" , "&" )
452- .replace ("{" , "{" )
453- .replace ("}" , "}" )
454- .replace ("[" , "[" )
455- .replace ("]" , "]" )
456- .replace (":" , ":" )
457- .replace ("," , "," );
458- }
459-
460370 private static List <StepResult > flattenSteps (List <StepResult > rootSteps ) {
461371 List <StepResult > allSteps = new ArrayList <>();
462372 if (rootSteps == null ) {
0 commit comments